diff --git a/packages/main/cypress/specs/RangeSlider.cy.tsx b/packages/main/cypress/specs/RangeSlider.cy.tsx index bbb533452e56..af0341671500 100755 --- a/packages/main/cypress/specs/RangeSlider.cy.tsx +++ b/packages/main/cypress/specs/RangeSlider.cy.tsx @@ -1863,3 +1863,147 @@ describe("Accessibility", () => { }); }); }); + +describe("Custom Tickmarks", () => { + const customTickmarks = [ + { value: 0, label: "Freezing" }, + { value: 25, label: "Cool" }, + { value: 50, label: "Warm" }, + { value: 75, label: "Hot" }, + { value: 100, label: "Boiling" }, + ]; + + beforeEach(() => { + cy.get('[data-cy-root]') + .invoke('css', 'padding', '100px'); + }); + + it("Renders custom labels on tickmarks", () => { + cy.mount( + + ); + + cy.get("[ui5-range-slider]") + .shadow() + .find("[ui5-slider-scale]") + .shadow() + .find(".ui5-slider-scale-tickmark-label") + .should("have.length", 5); + + cy.get("[ui5-range-slider]") + .shadow() + .find("[ui5-slider-scale]") + .shadow() + .find(".ui5-slider-scale-tickmark-label") + .eq(0) + .should("have.text", "Freezing"); + + cy.get("[ui5-range-slider]") + .shadow() + .find("[ui5-slider-scale]") + .shadow() + .find(".ui5-slider-scale-tickmark-label") + .eq(4) + .should("have.text", "Boiling"); + }); + + it("Allows free movement based on step, not snapping to tickmarks", () => { + cy.mount( + + ); + + cy.get("[ui5-range-slider]").as("slider"); + + cy.get("@slider").shadow().find("[ui5-slider-handle][handle-type='Start']").realClick(); + cy.get("@slider").shadow().find("[ui5-slider-handle][handle-type='Start']").realPress("ArrowRight"); + + cy.get("@slider").should("have.attr", "start-value", "1"); + }); + + it("Arrow key moves by step value for end handle", () => { + cy.mount( + + ); + + cy.get("[ui5-range-slider]").as("slider"); + + cy.get("@slider").shadow().find("[ui5-slider-handle][handle-type='End']").realClick(); + cy.get("@slider").shadow().find("[ui5-slider-handle][handle-type='End']").realPress("ArrowRight"); + + cy.get("@slider").should("have.attr", "end-value", "55"); + }); + + it("aria-valuetext reflects the custom label when value matches a tickmark", () => { + cy.mount( + + ); + + cy.get("[ui5-range-slider]") + .shadow() + .find("[ui5-slider-handle][handle-type='Start']") + .should("have.attr", "aria-valuetext", "Cool"); + + cy.get("[ui5-range-slider]") + .shadow() + .find("[ui5-slider-handle][handle-type='End']") + .should("have.attr", "aria-valuetext", "Hot"); + }); + + it("aria-valuetext is absent when value does not match a tickmark", () => { + cy.mount( + + ); + + cy.get("[ui5-range-slider]") + .shadow() + .find("[ui5-slider-handle][handle-type='Start']") + .should("not.have.attr", "aria-valuetext"); + + cy.get("[ui5-range-slider]") + .shadow() + .find("[ui5-slider-handle][handle-type='End']") + .should("not.have.attr", "aria-valuetext"); + }); + + it("Tooltip shows custom label when value matches a tickmark", () => { + cy.mount( + + ); + + cy.get("[ui5-range-slider]").as("slider"); + cy.get("@slider").shadow().find("[ui5-slider-handle][handle-type='Start']").realClick(); + + cy.get("@slider") + .shadow() + .find("[ui5-slider-tooltip][data-sap-ui-start-value]") + .should("have.attr", "value", "Cool"); + + cy.get("@slider") + .shadow() + .find("[ui5-slider-tooltip][data-sap-ui-end-value]") + .should("have.attr", "value", "Hot"); + }); + + it("Tickmarks auto-show without showTickmarks attribute", () => { + cy.mount( + + ); + + cy.get("[ui5-range-slider]") + .shadow() + .find("[ui5-slider-scale]") + .shadow() + .find(".ui5-slider-scale-tickmark") + .should("have.length.at.least", 5); + }); + + it("Backward compatibility - range slider without tickmarks works as before", () => { + cy.mount(); + + cy.get("[ui5-range-slider]").as("slider"); + cy.get("@slider").shadow().find("[ui5-slider-handle][handle-type='End']").realClick(); + + cy.get("@slider").shadow().find("[ui5-slider-handle][handle-type='End']").realPress("ArrowRight"); + cy.get("@slider").should("have.attr", "end-value", "8"); + }); +}); diff --git a/packages/main/cypress/specs/Slider.cy.tsx b/packages/main/cypress/specs/Slider.cy.tsx index 6cc1da2c631c..3b720c851422 100644 --- a/packages/main/cypress/specs/Slider.cy.tsx +++ b/packages/main/cypress/specs/Slider.cy.tsx @@ -1036,3 +1036,163 @@ describe("Testing resize handling and RTL support", () => { cy.get("@slider").should("have.value", 1); }); }); + +describe("Custom Values", () => { + const customTickmarks = [ + { value: 0, label: "Freezing" }, + { value: 25, label: "Room Temp" }, + { value: 50, label: "Warm" }, + { value: 100, label: "Boiling" }, + ]; + + beforeEach(() => { + cy.get('[data-cy-root]') + .invoke('css', 'padding', '100px'); + }); + + it("Renders custom labels on tickmarks", () => { + cy.mount( + + ); + + cy.get("[ui5-slider]") + .shadow() + .find("[ui5-slider-scale]") + .shadow() + .find(".ui5-slider-scale-tickmark-label") + .should("have.length", 4); + + cy.get("[ui5-slider]") + .shadow() + .find("[ui5-slider-scale]") + .shadow() + .find(".ui5-slider-scale-tickmark-label") + .eq(0) + .should("have.text", "Freezing"); + + cy.get("[ui5-slider]") + .shadow() + .find("[ui5-slider-scale]") + .shadow() + .find(".ui5-slider-scale-tickmark-label") + .eq(3) + .should("have.text", "Boiling"); + }); + + it("Allows free movement based on step, not snapping to tickmarks", () => { + cy.mount( + + ); + + cy.get("[ui5-slider]").as("slider"); + cy.get("@slider").realClick({ position: "left" }); + + cy.get("@slider").realPress("ArrowRight"); + cy.get("@slider").should("have.value", 1); + + cy.get("@slider").realPress("ArrowRight"); + cy.get("@slider").should("have.value", 2); + }); + + it("Arrow key moves by step value", () => { + cy.mount( + + ); + + cy.get("[ui5-slider]").as("slider"); + cy.get("@slider").realClick({ position: "left" }); + cy.get("@slider").should("have.value", 0); + + cy.get("@slider").realPress("ArrowRight"); + cy.get("@slider").should("have.value", 5); + + cy.get("@slider").realPress("ArrowRight"); + cy.get("@slider").should("have.value", 10); + }); + + it("Home/End keys jump to min/max", () => { + cy.mount( + + ); + + cy.get("[ui5-slider]").as("slider"); + cy.get("@slider").realClick(); + + cy.get("@slider").realPress("Home"); + cy.get("@slider").should("have.value", 0); + + cy.get("@slider").realPress("End"); + cy.get("@slider").should("have.value", 100); + }); + + it("Handle position is correct for non-uniform tickmark spacing", () => { + cy.mount( + + ); + + // value=25, min=0, max=100 → position should be 25% + cy.get("[ui5-slider]") + .shadow() + .find("[ui5-slider-handle]") + .should("have.attr", "style", "inset-inline-start: clamp(0%, 25%, 100%);"); + }); + + it("aria-valuetext reflects the custom label when value matches a tickmark", () => { + cy.mount( + + ); + + cy.get("[ui5-slider]") + .shadow() + .find("[ui5-slider-handle]") + .should("have.attr", "aria-valuetext", "Room Temp"); + }); + + it("aria-valuetext is absent when value does not match a tickmark", () => { + cy.mount( + + ); + + cy.get("[ui5-slider]") + .shadow() + .find("[ui5-slider-handle]") + .should("not.have.attr", "aria-valuetext"); + }); + + it("Tooltip shows custom label when value matches a tickmark", () => { + cy.mount( + + ); + + cy.get("[ui5-slider]").as("slider"); + cy.get("@slider").shadow().find("[ui5-slider-handle]").realClick(); + + cy.get("@slider") + .shadow() + .find("[ui5-slider-tooltip]") + .should("have.attr", "value", "Room Temp"); + }); + + it("Tickmarks auto-show without showTickmarks attribute", () => { + cy.mount( + + ); + + cy.get("[ui5-slider]") + .shadow() + .find("[ui5-slider-scale]") + .shadow() + .find(".ui5-slider-scale-tickmark") + .should("have.length.at.least", 4); + }); + + it("Backward compatibility - slider without tickmarks works as before", () => { + cy.mount(); + + cy.get("[ui5-slider]").as("slider"); + cy.get("@slider").realClick(); + + cy.get("@slider").realPress("ArrowRight"); + cy.get("@slider").should("have.value", 6); + }); +}); diff --git a/packages/main/src/RangeSlider.ts b/packages/main/src/RangeSlider.ts index d450ade72b1b..215be90694c5 100644 --- a/packages/main/src/RangeSlider.ts +++ b/packages/main/src/RangeSlider.ts @@ -15,6 +15,7 @@ import { getAssociatedLabelForTexts } from "@ui5/webcomponents-base/dist/util/Ac import SliderBase from "./SliderBase.js"; import RangeSliderTemplate from "./RangeSliderTemplate.js"; import type SliderTooltip from "./SliderTooltip.js"; +import type { Tickmark } from "./SliderScale.js"; // Texts import { @@ -107,7 +108,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { @property({ type: Number }) set startValue(value: number) { this._startValue = value; - this.tooltipStartValue = value?.toString() ?? ""; + this.tooltipStartValue = this._getCustomLabel(value) || (value?.toString() ?? ""); } get startValue(): number { @@ -124,7 +125,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { @property({ type: Number }) set endValue(value: number) { this._endValue = value; - this.tooltipEndValue = value?.toString() ?? ""; + this.tooltipEndValue = this._getCustomLabel(value) || (value?.toString() ?? ""); } get endValue(): number { @@ -143,6 +144,24 @@ class RangeSlider extends SliderBase implements IFormInputElement { @property() tooltipEndValueState: `${ValueState}` = "None"; + /** + * Defines custom tickmarks with labels on the slider scale. + * Each tickmark object has a numeric `value` and an optional `label` string. + * Tickmarks are purely visual — they display labeled markers at specific positions + * but do not affect the slider's movement behavior. The slider still moves + * according to `min`, `max`, and `step`. + * + * When the current value matches a tickmark value, the tickmark's label + * is shown in the tooltip and announced via `aria-valuetext`. + * + * **Note:** When `tickmarks` is provided, the scale is automatically shown + * (equivalent to `showTickmarks`). + * @default [] + * @public + */ + @property({ type: Array }) + tickmarks: Array = []; + @property({ type: Boolean }) rangePressed = false; @@ -226,6 +245,22 @@ class RangeSlider extends SliderBase implements IFormInputElement { return this.disabled || undefined; } + get _hasCustomTickmarks(): boolean { + return this.tickmarks.length > 0; + } + + _getCustomLabel(value: number): string | undefined { + return this.tickmarks.find(t => t.value === value)?.label; + } + + get _ariaValueTextStart(): string | undefined { + return this._getCustomLabel(this.startValue) || undefined; + } + + get _ariaValueTextEnd(): string | undefined { + return this._getCustomLabel(this.endValue) || undefined; + } + get _ariaLabelledByText() { return RangeSlider.i18nBundle.getText(RANGE_SLIDER_ARIA_DESCRIPTION); } @@ -429,8 +464,8 @@ class RangeSlider extends SliderBase implements IFormInputElement { this.update(affectedValue, newStartValue, newEndValue); } - this.tooltipStartValue = this.startValue.toString(); - this.tooltipEndValue = this.endValue.toString(); + this.tooltipStartValue = this._getCustomLabel(this.startValue) || this.startValue.toString(); + this.tooltipEndValue = this._getCustomLabel(this.endValue) || this.endValue.toString(); } /** @@ -597,8 +632,8 @@ class RangeSlider extends SliderBase implements IFormInputElement { // Updates UI and state when dragging of the whole selected range this._updateValueOnRangeDrag(e); - this.tooltipStartValue = this.startValue.toString(); - this.tooltipEndValue = this.endValue.toString(); + this.tooltipStartValue = this._getCustomLabel(this.startValue) || this.startValue.toString(); + this.tooltipEndValue = this._getCustomLabel(this.endValue) || this.endValue.toString(); } /** @@ -966,8 +1001,8 @@ class RangeSlider extends SliderBase implements IFormInputElement { return; } - this.tooltipStartValue = this.startValue.toString(); - this.tooltipEndValue = this.endValue.toString(); + this.tooltipStartValue = this._getCustomLabel(this.startValue) || this.startValue.toString(); + this.tooltipEndValue = this._getCustomLabel(this.endValue) || this.endValue.toString(); } _onTooltipInput(e: CustomEvent) { diff --git a/packages/main/src/RangeSliderTemplate.tsx b/packages/main/src/RangeSliderTemplate.tsx index 3d764751ff57..e996489e3422 100644 --- a/packages/main/src/RangeSliderTemplate.tsx +++ b/packages/main/src/RangeSliderTemplate.tsx @@ -28,6 +28,7 @@ const startHandle = (slider: RangeSlider) => { aria-valuemin={slider.min} aria-valuemax={slider.max} aria-valuenow={slider.startValue} + aria-valuetext={slider._ariaValueTextStart} aria-label={slider._ariaLabelStartHandle} aria-disabled={slider._ariaDisabled} aria-describedby={slider._ariaDescribedByHandleText} @@ -62,6 +63,7 @@ const endHandle = (slider: RangeSlider) => { aria-valuemin={slider.min} aria-valuemax={slider.max} aria-valuenow={slider.endValue} + aria-valuetext={slider._ariaValueTextEnd} aria-label={slider._ariaLabelEndHandle} aria-disabled={slider._ariaDisabled} aria-describedby={slider._ariaDescribedByHandleText} @@ -134,8 +136,9 @@ export default function RangeSliderTemplate(this: RangeSlider) { min={this.min} max={this.max} step={this._effectiveStep} - showTickmarks={this.showTickmarks} - labelInterval={this.labelInterval} + showTickmarks={this.showTickmarks || this._hasCustomTickmarks} + labelInterval={this._hasCustomTickmarks ? 1 : this.labelInterval} + tickmarks={this.tickmarks} progressTabIndex={this._tabIndex} progressAriaValueNow={this._ariaValueNow} progressAriaValueText={`From ${this.startValue} to ${this.endValue}`} diff --git a/packages/main/src/Slider.ts b/packages/main/src/Slider.ts index 2b39cd2747cd..a9765a2ae970 100644 --- a/packages/main/src/Slider.ts +++ b/packages/main/src/Slider.ts @@ -7,6 +7,7 @@ import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/In import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import SliderBase from "./SliderBase.js"; import type SliderTooltip from "./SliderTooltip.js"; +import type { Tickmark } from "./SliderScale.js"; // Template import SliderTemplate from "./SliderTemplate.js"; @@ -102,6 +103,24 @@ class Slider extends SliderBase implements IFormInputElement { @property({ type: Number }) step = 1; + /** + * Defines custom tickmarks with labels on the slider scale. + * Each tickmark object has a numeric `value` and an optional `label` string. + * Tickmarks are purely visual — they display labeled markers at specific positions + * but do not affect the slider's movement behavior. The slider still moves + * according to `min`, `max`, and `step`. + * + * When the current value matches a tickmark value, the tickmark's label + * is shown in the tooltip and announced via `aria-valuetext`. + * + * **Note:** When `tickmarks` is provided, the scale is automatically shown + * (equivalent to `showTickmarks`). + * @default [] + * @public + */ + @property({ type: Array }) + tickmarks: Array = []; + @property() tooltipValueState: `${ValueState}` = "None"; @@ -118,6 +137,19 @@ class Slider extends SliderBase implements IFormInputElement { return this.value.toString(); } + get _hasCustomTickmarks(): boolean { + return this.tickmarks.length > 0; + } + + get _ariaValueText(): string | undefined { + const label = this._getCustomLabel(this.value); + return label || undefined; + } + + _getCustomLabel(value: number): string | undefined { + return this.tickmarks.find(t => t.value === value)?.label; + } + @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; @@ -131,7 +163,6 @@ class Slider extends SliderBase implements IFormInputElement { * @private */ onBeforeRendering() { - // Clamp value visually without modifying the actual value property const ctor = this.constructor as typeof Slider; const clampedValue = ctor.clipValue(this.value, this.min, this.max); this._updateHandleAndProgress(clampedValue); @@ -148,8 +179,6 @@ class Slider extends SliderBase implements IFormInputElement { * @private */ _onmousedown(e: TouchEvent | MouseEvent) { - // If step is 0 no interaction is available because there is no constant - // (equal for all user environments) quantitative representation of the value if (this.disabled || this.step === 0 || (e.target as HTMLElement).hasAttribute("ui5-slider-tooltip")) { return; } @@ -157,19 +186,16 @@ class Slider extends SliderBase implements IFormInputElement { const newValue = this.handleDownBase(e); this._valueOnInteractionStart = this.value; - // Set initial value if one is not set previously on focus in. - // It will be restored if ESC key is pressed. if (this._valueInitial === undefined) { this._valueInitial = this.value; } - // Do not yet update the Slider if press is over a handle. It will be updated if the user drags the mouse. const ctor = this.constructor as typeof Slider; if (!this._isHandlePressed(ctor.getPageXValueFromEvent(e))) { const stepPrecision = ctor._getDecimalPrecisionOfNumber(this.step); this._updateHandleAndProgress(newValue); this.value = newValue; - this.tooltipValue = newValue.toFixed(stepPrecision); + this.tooltipValue = this._getCustomLabel(newValue) || newValue.toFixed(stepPrecision); this.updateStateStorageAndFireInputEvent("value"); } } @@ -236,6 +262,11 @@ class Slider extends SliderBase implements IFormInputElement { } _onTooltipOpen() { + const customLabel = this._getCustomLabel(this.value); + if (customLabel) { + this.tooltipValue = customLabel; + return; + } const ctor = this.constructor as typeof Slider; const stepPrecision = ctor._getDecimalPrecisionOfNumber(this.step); this.tooltipValue = this.value.toFixed(stepPrecision); @@ -258,7 +289,7 @@ class Slider extends SliderBase implements IFormInputElement { this._updateHandleAndProgress(newValue); this.value = newValue; - this.tooltipValue = newValue.toFixed(stepPrecision); + this.tooltipValue = this._getCustomLabel(newValue) || newValue.toFixed(stepPrecision); this.updateStateStorageAndFireInputEvent("value"); } @@ -301,9 +332,7 @@ class Slider extends SliderBase implements IFormInputElement { const max = this.max; const min = this.min; - // The progress (completed) percentage of the slider. this._progressPercentage = (newValue - min) / (max - min); - // How many pixels from the left end of the slider will be the placed the affected by the user action handle this._handlePositionFromStart = this._progressPercentage * 100; } @@ -318,7 +347,7 @@ class Slider extends SliderBase implements IFormInputElement { const stepPrecision = ctor._getDecimalPrecisionOfNumber(this.step); this._updateHandleAndProgress(newValue!); this.value = newValue!; - this.tooltipValue = this.value.toFixed(stepPrecision); + this.tooltipValue = this._getCustomLabel(newValue!) || this.value.toFixed(stepPrecision); this.updateStateStorageAndFireInputEvent("value"); } } diff --git a/packages/main/src/SliderScale.ts b/packages/main/src/SliderScale.ts index 72547b876edd..53166258de99 100644 --- a/packages/main/src/SliderScale.ts +++ b/packages/main/src/SliderScale.ts @@ -96,7 +96,7 @@ class SliderScale extends UI5Element { /** * Defines custom tickmarks to be displayed on the scale. * @default [] - * @private + * @public */ @property({ type: Array }) tickmarks: Array = []; diff --git a/packages/main/src/SliderTemplate.tsx b/packages/main/src/SliderTemplate.tsx index 2359c5559ba9..9df1c6804c90 100644 --- a/packages/main/src/SliderTemplate.tsx +++ b/packages/main/src/SliderTemplate.tsx @@ -27,6 +27,7 @@ const handle = (slider: Slider) => { aria-valuemin={slider.min} aria-valuemax={slider.max} aria-valuenow={slider.value} + aria-valuetext={slider._ariaValueText} aria-label={slider._ariaLabel} aria-disabled={slider._ariaDisabled} aria-describedby={slider._ariaDescribedByHandleText} @@ -77,8 +78,9 @@ export default function SliderTemplate(this: Slider) { max={this.max} step={this.step} startValue={this.min} - showTickmarks={this.showTickmarks} - labelInterval={this.labelInterval} + showTickmarks={this.showTickmarks || this._hasCustomTickmarks} + labelInterval={this._hasCustomTickmarks ? 1 : this.labelInterval} + tickmarks={this.tickmarks} onFocusOut={this._onfocusout} onFocusIn={this._onfocusin} part="scale" diff --git a/packages/main/test/pages/RangeSlider.html b/packages/main/test/pages/RangeSlider.html index 58ca1466d64c..95c8bd37b3f0 100644 --- a/packages/main/test/pages/RangeSlider.html +++ b/packages/main/test/pages/RangeSlider.html @@ -48,6 +48,14 @@

Range Slider with steps, input tooltips, tickmarks +
+

Custom Tickmarks - Temperature Range

+ + +

Custom Tickmarks - Price Range

+ +
+

Event Testing Slider

@@ -72,6 +80,23 @@

Event Testing Result Slider

diff --git a/packages/main/test/pages/SliderCustomTickmarks.html b/packages/main/test/pages/SliderCustomTickmarks.html new file mode 100644 index 000000000000..326b14c286fb --- /dev/null +++ b/packages/main/test/pages/SliderCustomTickmarks.html @@ -0,0 +1,215 @@ + + + + + + Slider Custom Tickmarks - Design Questions + + + + + + + + + + +

Slider Custom Tickmarks — Design Review

+

Interactive examples for the open design questions. Try each slider with mouse, keyboard (Arrow, Ctrl+Arrow, Home/End), and observe the tooltip behavior.

+ + +
+

1. Tooltip for in-between values

+
Drag the handle between labeled tickmarks. The tooltip shows the raw number (e.g. "26") when not on a tickmark, and the label (e.g. "Room Temp") when exactly on one.
+ +
+ Question: When the handle is between labeled positions (e.g. value=30, between "Room Temp" at 25 and "Warm" at 50), showing the raw number is correct? Or should we show the nearest label, both neighbors, or something else? +
+
+ + +
+

2. Snap / magnetic tickmarks

+
Drag the handle freely — it moves by step=1 with no snapping. Compare with the second slider where step matches tickmark spacing (effectively snapping).
+

Free movement (step=1, 100 positions):

+ +

Effectively snapping (step=33, only lands on tickmarks):

+ +
+ Question: Should there be a subtle "magnetic" snap zone (e.g. within 2-3px of a tickmark) that pulls the handle to the labeled value for easier selection? Or is free movement preferred with the app controlling step to achieve snap-like behavior? +
+
+ + +
+

3. Keyboard large-step behavior (Ctrl+Arrow)

+
Focus the slider and press Ctrl+Right Arrow. Currently it jumps by 10 (1/10th of range). The tickmarks are at 0, 33, 66, 100.
+ +
+ Question: Should Ctrl+Arrow jump to the next/previous tickmark value instead (0→33→66→100)? That would make labeled points reachable in one keystroke. Or keep the standard 1/10th jump? +
+
+ + +
+

4. Minor ticks between labeled tickmarks

+
First slider: only custom tickmarks render. Second slider: showTickmarks is also set, but step-based ticks don't show alongside custom ones.
+

Custom tickmarks only:

+ +

With show-tickmarks also set:

+ +
+ Question: When custom labeled tickmarks are defined with a fine step, should step-based minor ticks also be visible between the labeled major ticks? Or are only the labeled positions shown? +
+
+ + +
+

5. Label density and overflow

+
This slider has many tickmarks with long labels. Resize the browser window narrower to see how labels behave when space is limited.
+ +
+ Question: When labels overlap, the current behavior hides them (only first/last remain). Is this acceptable? Should we truncate, rotate, or apply other responsive rules? +
+
+ + +
+

6. Range Slider: in-range label emphasis

+
The tickmarks between the two handles get "in-range" coloring on the tick line. Observe the labels — they have no visual change.
+ +
+ Question: Should tickmark labels within the selected range get visual emphasis (bold, different color) to reinforce the selection? Or keep them neutral? +
+
+ + + +