Primitives
Slider
A draggable thumb that picks a numeric value along a track.
Drag a thumb, or focus it and press the arrow keys. Home and End jump to the bounds, and each thumb carries its own data-index.
A single primitive supports single, range, and multi-thumb sliders: the shape comes from value's array length and how many [forSliderThumb] you render. Implements FormValueControl<readonly number[]> from @angular/forms/signals.
Anatomy
<div forSlider [(value)]="value" [min]="0" [max]="100" [step]="1">
<span forSliderTrack>
<span forSliderRange></span>
<span forSliderThumb [index]="0" ariaLabel="Volume"></span>
</span>
</div>
Examples
Stepped
step sets the granularity values snap to and the amount each arrow-key press moves. Here step is 10, so values snap to 0, 10, 20…; PageUp / PageDown move by 10× this step.
Range (two thumbs)
The value model is a readonly number[]; two forSliderThumb pieces, one per index, make a range. Each thumb's aria-valuemin / aria-valuemax squeeze to its neighbor so the thumbs can't cross, and minStepsBetweenThumbs keeps a minimum gap in step units.
Vertical orientation
orientation='vertical' reflects data-orientation on every piece and sets aria-orientation on the thumb. The exposed fractions are unchanged, and the consumer paints them along the Y axis. ArrowUp increases the value and ArrowDown decreases it.
Inverted
inverted flips the value-to-position mapping: in horizontal LTR, max sits on the left. The flip is baked into the exposed fractions, so the same CSS paints both ways. Keyboard semantics are unchanged: ArrowRight / ArrowUp still move toward max.
Signal Forms
[forSlider] implements FormValueControl<readonly number[]>. Pair with [formField] for auto-wiring with @angular/forms/signals:
<div forSlider [formField]="form.opacity">…</div>
For native <form> submit, set [name] and the directive mirrors value() into N <input type="hidden"> siblings (one per thumb). data-touched / data-dirty / data-pending / data-invalid are reflected on the host as boolean data-* attributes (present when true, absent otherwise).
API
ForSlider
| Property | Type | Description |
|---|---|---|
value | model | Two-way bindable value array. Required by FormValueControl<readonly number[]>. Output. Emits on drag, keyboard, and track click.Default: — |
min | input | Numeric minimum. Default: 0 |
max | input | Numeric maximum. Default: 100 |
step | input | Discrete value increment for arrows + drag. Default: 1 |
stepMultiplier | input | Multiplier over step for PageUp / PageDown (configurable via provideForSliderDefaults).Default: 10 |
orientation | input | 'horizontal' or 'vertical'.Default: 'horizontal' |
dir | input | 'ltr' or 'rtl'. RTL flips horizontal pointer mapping and ArrowLeft/ArrowRight semantics.Default: 'ltr' |
inverted | input | Visually flips the value-to-position mapping (e.g. max on the left in horizontal LTR). Keyboard Up/Right (LTR) still moves toward max regardless.Default: false |
minStepsBetweenThumbs | input | Multi-thumb only: minimum gap between adjacent thumbs in step units. Default: 0 |
disabled | input | Disables all interaction. Default: false |
readonly | input | Allows focus + announcement, blocks updates. Default: false |
name | input | If non-empty, mirrors value() into N <input type="hidden"> siblings for native form submit.Default: '' |
valueCommit | — | Output. Fires once with the final value array at the trailing edge of a value-changing interaction: on pointerup / pointercancel after a drag, or on keyup after one or more keyboard adjustments.Default: — |
touchedChange | — | Output. Fires once, the first time the slider is touched (drag end, or focus leaving the slider region). Default: — |
touch | — | Output. Signal Forms touch notification. Fires on every touch-producing interaction (drag end, focus leaving the region), not only the first. Default: — |
(valueChange) (from model<readonly number[]>) fires only on internal updates (drag, keyboard, track click). It stays silent on consumer writes via [(value)].
(valueCommit) stays silent when the interaction did not actually change the value (e.g. press + release without movement, or arrow at the extreme).
| Data attribute | Values | |
|---|---|---|
data-orientation | horizontal | vertical | |
data-disabled | present | absent | |
data-readonly | present | absent | |
data-touched | present | absent | |
data-dirty | present | absent | |
data-pending | present | absent | |
data-invalid | present | absent |
ForSliderTrack
The clickable track surface. PointerDown anywhere on the track that isn't a thumb finds the nearest thumb, jumps it to that position, focuses it, and starts a drag.
| Data attribute | Values | |
|---|---|---|
data-orientation | horizontal | vertical | |
data-disabled | present | absent |
ForSliderRange
Optional decorative band between the lowest and highest thumb (single-thumb: 0 → thumb; multi-thumb: between the outermost thumbs). Exposes --for-slider-range-start, --for-slider-range-end, and --for-slider-range-size for sizing.
| Data attribute | Values | |
|---|---|---|
data-orientation | horizontal | vertical | |
data-disabled | present | absent |
ForSliderThumb
| Property | Type | Description |
|---|---|---|
index | input.required | 0-based position in the parent slider's value() array. Pass the loop index when rendering N thumbs.Default: — |
ariaLabel | input | Accessible name for the thumb, emitted as aria-label only when non-empty. A consumer-set static aria-label wins.Default: null |
valueText | input | Human-readable value override (e.g. $1,200 instead of 1200), mirrored as aria-valuetext only when non-empty.Default: '' |
| Data attribute | Values | |
|---|---|---|
data-orientation | horizontal | vertical | |
data-disabled | present | absent | |
data-readonly | present | absent | |
data-index | 0-based thumb index |
Keyboard
Focus a thumb, then:
| Key | Action | |
|---|---|---|
| ArrowRight (LTR) / ArrowLeft (RTL) / ArrowUp | Increase by step. | |
| ArrowLeft (LTR) / ArrowRight (RTL) / ArrowDown | Decrease by step. | |
| PageUp | Increase by step × stepMultiplier. | |
| PageDown | Decrease by step × stepMultiplier. | |
| Home | Set to min. | |
| End | Set to max. |
inverted swaps "increase" / "decrease" on every key. Disabled and readonly thumbs are no-ops.
Values live on the min ± k·step grid. A thumb already on the grid travels the full amount (step, or step × stepMultiplier for the page keys); a thumb off the grid lands on the adjacent grid point in the direction of travel and the page multiplier is discarded, matching the platform's HTMLInputElement.stepUp() / stepDown(). So ArrowRight from 23 with [step]="10" gives 30 and ArrowLeft gives 20. The first jump is never oversized. Pointer drags are unaffected: they snap to the nearest grid point, since a drag has no direction of travel.
Accessibility
Implements the WAI-ARIA Slider pattern (single thumb) and the WAI-ARIA Slider (Multi-Thumb) pattern (range / N thumbs).
role="slider"on each thumb witharia-valuemin,aria-valuemax,aria-valuenow, optionalaria-valuetext, andaria-orientation.- Multi-thumb non-passing: each thumb's
aria-valuemin/aria-valuemaxautomatically squeeze to its neighbors' values (offset by theminStepsBetweenThumbsgap), so the announced range is exactly the range the thumb can reach, matching the APG multi-thumb guidance. - The root has
role="group"anddir="rtl"mirrored whendir()==='rtl', so screen readers and CSS layout agree. disabledthumbs receivetabindex="-1"andaria-disabled="true".aria-readonlybelongs on the thumb, not the root. WAI-ARIA supports it onrole="slider"but not onrole="group", so each thumb carriesaria-readonly="true"while the root reflects thedata-readonlystyling hook only.- Provide
[ariaLabel]on every thumb. Even single-thumb sliders benefit from explicit naming. The directive does not synthesize a label. When the name is already visible in the DOM, write a nativearia-labelledbyon the thumb instead; the directive leaves that attribute alone. - A degenerate configuration (
mingreater thanmax, or a non-positivestep) leaves the slider inoperable and is dev-guarded by aconsole.warnin development builds.
Styling
forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).
CSS custom properties
| Property | Meaning | |
|---|---|---|
--for-slider-thumb-position | On [forSliderThumb]. Fraction [0, 1], already accounting for inverted. The thumb's position along the track. | |
--for-slider-range-start | On [forSliderRange]. Lowest fraction [0, 1] (single: pinned to the closer edge; multi: smallest thumb). | |
--for-slider-range-end | On [forSliderRange]. Highest fraction [0, 1]. | |
--for-slider-range-size | On [forSliderRange]. end - start. Useful for width / height. |
Pair with data-orientation on every piece to pick the right axis from CSS.
.sl-thumb {
inset-inline-start: calc(var(--for-slider-thumb-position) * 100%);
}
.sl-range {
inset-inline-start: calc(var(--for-slider-range-start) * 100%);
inline-size: calc(var(--for-slider-range-size) * 100%);
}
.sl-thumb[data-disabled] {
opacity: 0.5;
}
Wrapping in a design system
Wrapping form primitives documents both supported wrapper patterns: hostDirectives with the exported FOR_SLIDER_HOST_DIRECTIVE_INPUTS / FOR_SLIDER_HOST_DIRECTIVE_OUTPUTS name tuples, and subclassing.