Form
Slider
Usage#
import { WdsSliderComponent } from '@wds/angular/slider';Examples#
Default#
value between min (0) and max (100), moved by step. Without a visible label, name the slider with label.
import { Component } from '@angular/core';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-default-demo',
imports: [WdsSliderComponent],
template: `
<wds-slider class="w-full max-w-xs" value="50" max="100" step="1" label="Volume"></wds-slider>
`,
})
export class SliderDefaultDemo {}Disabled#
disabled dims the slider and excludes it from the keyboard and the form.
import { Component } from '@angular/core';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-disabled-demo',
imports: [WdsSliderComponent],
template: `
<wds-slider class="w-full max-w-xs" value="50" max="100" step="1" label="Volume" disabled></wds-slider>
`,
})
export class SliderDisabledDemo {}Range#
values with two numbers gives two thumbs; the track between them is filled. The attribute takes a list: values="25,75".
import { Component } from '@angular/core';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-range-demo',
imports: [WdsSliderComponent],
template: `
<wds-slider class="w-full max-w-xs" values="25,75" max="100" step="5" label="Price"></wds-slider>
`,
})
export class SliderRangeDemo {}Multiple thumbs#
import { Component } from '@angular/core';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-multiple-demo',
imports: [WdsSliderComponent],
template: `
<wds-slider class="w-full max-w-xs" values="10,40,80" max="100" step="10" label="Breakpoints"></wds-slider>
`,
})
export class SliderMultipleDemo {}Vertical#
orientation="vertical" grows upward; give the slider a height (default 10rem).
import { Component } from '@angular/core';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-vertical-demo',
imports: [WdsSliderComponent],
template: `
<div class="flex items-center justify-center gap-12">
<wds-slider class="h-40" orientation="vertical" value="50" max="100" step="1" label="Volume"></wds-slider>
<wds-slider class="h-40" orientation="vertical" values="25,75" max="100" step="5" label="Range"></wds-slider>
</div>
`,
})
export class SliderVerticalDemo {}Controlled#
wds-input fires on every change while dragging, with value and values; wds-change fires once the thumb is released. Here a range from 0 to 1 with step="0.1".
import { Component, signal } from '@angular/core';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-controlled-demo',
host: { class: 'flex w-full justify-center' },
imports: [...WDS_FIELD, WdsSliderComponent],
template: `
<wds-field class="w-full max-w-sm">
<div class="flex items-center justify-between gap-2">
<wds-field-label>Controlled range</wds-field-label>
<span class="text-xs font-semibold tabular-nums">{{ range()[0].toFixed(1) }} – {{ range()[1].toFixed(1) }}</span>
</div>
<wds-slider [values]="range()" min="0" max="1" step="0.1" (wdsInput)="range.set($event.values)"></wds-slider>
</wds-field>
`,
})
export class SliderControlledDemo {
protected readonly range = signal([0.3, 0.7]);
}With input#
wds-input. The field shows the value with a percent sign (format-options).
import { Component, signal } from '@angular/core';
import { WdsNumberFieldComponent } from '@wds/angular/number-field';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-input-demo',
host: { class: 'flex w-full justify-center' },
imports: [WdsNumberFieldComponent, WdsSliderComponent],
template: `
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="flex items-center justify-between">
<span class="text-sm font-medium" aria-hidden="true">Opacity</span>
<wds-number-field
class="w-32"
min="0"
max="100"
format-options='{"style":"unit","unit":"percent"}'
label="Opacity"
[value]="opacity()"
(wdsInput)="fromField($event.value)"
/>
</div>
<wds-slider label="Opacity" [value]="opacity()" max="100" (wdsInput)="opacity.set($event.value)"></wds-slider>
</div>
`,
})
export class SliderInputDemo {
protected readonly opacity = signal(50);
/** While typing the field can hold an empty or out-of-range value; it is clamped to 0–100 when it loses focus. */
protected fromField(value: number | null): void {
if (value !== null && value >= 0 && value <= 100) this.opacity.set(value);
}
}Color temperature#
formatValue (a property) sets what screen readers announce (aria-valuetext), e.g. 4500 kelvin, Neutral.
import { Component, signal } from '@angular/core';
import { WdsSliderComponent } from '@wds/angular/slider';
/** Name of a color temperature in kelvins. */
function temperatureName(kelvin: number): string {
if (kelvin <= 3000) return 'Warm';
if (kelvin <= 4500) return 'Neutral';
if (kelvin <= 5500) return 'Daylight';
return 'Cool';
}
@Component({
selector: 'app-slider-temperature-demo',
host: { class: 'flex w-full justify-center' },
imports: [WdsSliderComponent],
template: `
<div class="grid w-full max-w-sm gap-4">
<div class="flex items-center justify-between">
<span class="text-sm font-medium" aria-hidden="true">Color temperature</span>
<span class="text-xs font-medium text-muted-foreground tabular-nums">{{ kelvin() }}K · {{ name(kelvin()) }}</span>
</div>
<div class="h-2 rounded-full bg-[linear-gradient(to_right,#ff8a2b,#ffd4a3,#fff5e6,#e8f0ff,#a3c9ff)]" aria-hidden="true"></div>
<wds-slider
label="Color temperature"
[value]="kelvin()"
min="2700"
max="6500"
step="100"
[formatValue]="format"
(wdsInput)="kelvin.set($event.value)"
></wds-slider>
</div>
`,
})
export class SliderTemperatureDemo {
protected readonly kelvin = signal(4500);
protected readonly name = temperatureName;
/** What screen readers announce instead of the bare number. */
protected readonly format = (kelvin: number) => `${kelvin} kelvin, ${temperatureName(kelvin)}`;
}Reference labels#
wds-field the wds-field-label names the slider. The labels under the track are decorative (aria-hidden).
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-reference-labels-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsSliderComponent],
template: `
<wds-field class="w-full max-w-sm">
<wds-field-label>Storage</wds-field-label>
<wds-slider value="15" min="5" max="35"></wds-slider>
<span class="flex w-full items-center justify-between text-xs font-medium text-muted-foreground" aria-hidden="true">
<span>5 GB</span>
<span>20 GB</span>
<span>35 GB</span>
</span>
</wds-field>
`,
})
export class SliderReferenceLabelsDemo {}Tick marks#
px-1.5).
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-ticks-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsSliderComponent],
template: `
<wds-field class="w-full max-w-sm">
<wds-field-label>Duration (months)</wds-field-label>
<wds-slider value="5" max="12"></wds-slider>
<span class="flex w-full items-center justify-between gap-1 px-1.5 text-xs font-medium text-muted-foreground" aria-hidden="true">
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-1"></span>
<span>0</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-0.5"></span>
<span class="opacity-0">1</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-1"></span>
<span>2</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-0.5"></span>
<span class="opacity-0">3</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-1"></span>
<span>4</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-0.5"></span>
<span class="opacity-0">5</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-1"></span>
<span>6</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-0.5"></span>
<span class="opacity-0">7</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-1"></span>
<span>8</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-0.5"></span>
<span class="opacity-0">9</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-1"></span>
<span>10</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-0.5"></span>
<span class="opacity-0">11</span>
</span>
<span class="flex w-0 flex-col items-center justify-center gap-2">
<span class="w-px bg-muted-foreground/70 h-1"></span>
<span>12</span>
</span>
</span>
</wds-field>
`,
})
export class SliderTicksDemo {}Value tooltip#
import { Component, computed, signal } from '@angular/core';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-tooltip-demo',
host: { class: 'flex w-full justify-center' },
imports: [WdsSliderComponent],
template: `
<div class="grid w-full max-w-sm gap-4">
<span class="text-sm font-medium" aria-hidden="true">Volume</span>
<div class="relative pt-8">
<!-- Above the center of the thumb: the thumb (12px) stays inside the track, as in wds-slider. -->
<div
class="absolute top-0 -translate-x-1/2 rounded bg-foreground px-2 py-0.5 text-xs font-semibold text-background tabular-nums"
[style.left]="left()"
aria-hidden="true"
>
{{ volume() }}%
<div class="absolute -bottom-1 left-1/2 size-2 -translate-x-1/2 rotate-45 bg-foreground"></div>
</div>
<wds-slider label="Volume" [value]="volume()" max="100" (wdsInput)="volume.set($event.value)"></wds-slider>
</div>
</div>
`,
})
export class SliderTooltipDemo {
protected readonly volume = signal(50);
protected readonly left = computed(() => `calc(6px + ${this.volume() / 100} * (100% - 12px))`);
}Rating#
formatValue makes screen readers announce the word.
import { Component, signal } from '@angular/core';
import { WdsSliderComponent } from '@wds/angular/slider';
const EMOJIS = ['😡', '🙁', '😐', '🙂', '😍'];
const LABELS = ['Awful', 'Poor', 'Okay', 'Good', 'Amazing'];
@Component({
selector: 'app-slider-rating-demo',
host: { class: 'flex w-full justify-center' },
imports: [WdsSliderComponent],
template: `
<div class="grid w-full max-w-sm gap-3">
<span class="text-sm font-medium" aria-hidden="true">Rate your experience</span>
<div class="flex items-center gap-3">
<wds-slider
label="Rate your experience"
[value]="rating()"
min="1"
max="5"
[formatValue]="format"
(wdsInput)="rating.set($event.value)"
></wds-slider>
<span class="text-2xl" aria-hidden="true">{{ emojis[rating() - 1] }}</span>
</div>
<span class="text-center text-xs font-medium text-muted-foreground" aria-hidden="true">{{ labels[rating() - 1] }}</span>
</div>
`,
})
export class SliderRatingDemo {
protected readonly emojis = EMOJIS;
protected readonly labels = LABELS;
protected readonly rating = signal(3);
/** Screen readers hear the word, e.g. "Okay, 3 of 5". */
protected readonly format = (value: number) => `${LABELS[value - 1]}, ${value} of 5`;
}Forms#
Integration with Angular forms. Pick the API your app uses.
Signal forms#
WdsSliderField connects the slider to [formField]. A number gives one thumb, an array of numbers one thumb per value; the value updates while dragging. With [formField] Angular does not allow min and max attributes: for a number set them in the schema (min(), max()), a range uses the default 0–100.
import { Component, signal } from '@angular/core';
import { FormField, FormRoot, form, max, min } from '@angular/forms/signals';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsSliderField } from '@wds/angular/forms';
import { WdsSliderComponent } from '@wds/angular/slider';
interface Filters {
price: number[];
rating: number;
}
@Component({
selector: 'app-slider-signal-forms-demo',
imports: [FormField, FormRoot, WdsButtonComponent, ...WDS_FIELD, WdsSliderComponent, WdsSliderField],
template: `
<form class="flex w-sm max-w-full flex-col gap-6" [formRoot]="filtersForm">
<wds-field>
<wds-field-label>Discount</wds-field-label>
<!-- An array value gives one thumb per number (here the default 0–100). -->
<wds-slider [formField]="filtersForm.price" step="5"></wds-slider>
<wds-field-description>{{ filters().price[0] }} – {{ filters().price[1] }}%</wds-field-description>
</wds-field>
<wds-field>
<wds-field-label>Minimum rating</wds-field-label>
<wds-slider [formField]="filtersForm.rating"></wds-slider>
</wds-field>
<wds-button type="submit" class="self-start">Apply</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class SliderSignalFormsDemo {
protected readonly filters = signal<Filters>({ price: [20, 60], rating: 3 });
protected readonly submitted = signal('—');
protected readonly filtersForm = form(
this.filters,
(path) => {
// With [formField] the bounds come from the schema, not from min/max attributes.
min(path.rating, 1);
max(path.rating, 5);
},
{
submission: {
action: async (form) => {
this.submitted.set(JSON.stringify(form().value()));
},
},
}
);
}Template-driven#
WdsSliderValueAccessor makes ngModel work on wds-slider. Inside a <form> each slider needs a name.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsSliderValueAccessor } from '@wds/angular/forms';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-template-driven-demo',
imports: [FormsModule, WdsButtonComponent, ...WDS_FIELD, WdsSliderComponent, WdsSliderValueAccessor],
template: `
<form class="flex w-sm max-w-full flex-col gap-6" (ngSubmit)="submitted.set(JSON.stringify({ price, rating }))">
<wds-field>
<wds-field-label>Price</wds-field-label>
<wds-slider name="price" [(ngModel)]="price" max="500" step="10"></wds-slider>
<wds-field-description>{{ price[0] }} – {{ price[1] }} USD</wds-field-description>
</wds-field>
<wds-field>
<wds-field-label>Minimum rating</wds-field-label>
<wds-slider name="rating" [(ngModel)]="rating" min="1" max="5"></wds-slider>
</wds-field>
<wds-button type="submit" class="self-start">Apply</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class SliderTemplateDrivenDemo {
protected price = [100, 300];
protected rating = 3;
protected readonly submitted = signal('—');
protected readonly JSON = JSON;
}Reactive forms#
WdsSliderValueAccessor works with formControl and formControlName; an array in the FormControl gives a range.
import { Component, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsSliderValueAccessor } from '@wds/angular/forms';
import { WdsSliderComponent } from '@wds/angular/slider';
@Component({
selector: 'app-slider-reactive-forms-demo',
imports: [ReactiveFormsModule, WdsButtonComponent, ...WDS_FIELD, WdsSliderComponent, WdsSliderValueAccessor],
template: `
<form class="flex w-sm max-w-full flex-col gap-6" [formGroup]="filtersForm" (ngSubmit)="save()">
<wds-field>
<wds-field-label>Price</wds-field-label>
<wds-slider formControlName="price" max="500" step="10"></wds-slider>
</wds-field>
<wds-field>
<wds-field-label>Minimum rating</wds-field-label>
<wds-slider formControlName="rating" min="1" max="5"></wds-slider>
</wds-field>
<wds-button type="submit" class="self-start">Apply</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class SliderReactiveFormsDemo {
protected readonly filtersForm = new FormGroup({
// An array value gives one thumb per number.
price: new FormControl([100, 300], { nonNullable: true }),
rating: new FormControl(3, { nonNullable: true }),
});
protected readonly submitted = signal('—');
protected save(): void {
this.submitted.set(JSON.stringify(this.filtersForm.getRawValue()));
}
}API References#
wds-slider#
values with two or more numbers gives a thumb for each (a range).
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| value | number | 0 | values has numbers. |
| values | number[] | [] | [25, 75] for a range. The attribute takes a list: values="25,75". |
| min | number | 0 | |
| max | number | 100 | |
| step | number | 1 | |
| orientation | WdsSliderOrientation | 'horizontal' | |
| disabled | boolean | false | |
| invalid | boolean | false | aria-invalid. |
| name | string | '' | |
| label | string | '' | wds-field with a label. |
| thumb-labels.thumbLabels | string[] | [] | Minimum, Maximum. |
Properties
| Property | Description |
|---|---|
| formatValue((value: number, index: number) => string) | null | aria-valuetext), e.g. (v) => v + ' GB'. |
Events
| Event | Description |
|---|---|
| wds-inputCustomEvent<WdsSliderChangeDetail> | |
| wds-changeCustomEvent<WdsSliderChangeDetail> |
Methods
| Method | Description |
|---|---|
| formResetCallback() | |
| formDisabledCallback(disabled: boolean) | |
| checkValidity() | true when the value is valid; otherwise fires invalid on the element. |
| reportValidity() | checkValidity(), and shows the browser validation bubble when the value is invalid. |
| setCustomValidity(message: string) | input.setCustomValidity(). |
CSS parts (::part)
| Part | Description |
|---|---|
| control | |
| track | |
| range | |
| thumb | role="slider"). |
CSS custom properties
| Property | Description |
|---|---|
| --wds-slider-track-size | |
| --wds-slider-track | |
| --wds-slider-range | |
| --wds-slider-thumb |
Accessibility#
Screen reader#
slider with aria-valuenow, aria-valuemin, aria-valuemax and aria-orientation. formatValue adds aria-valuetext, so units or words are read instead of the bare number.
label, or by the wds-field-label of a wds-field. With two thumbs the names get Minimum and Maximum (change them with thumb-labels), with more 1 of 3 and so on.
invalid sets aria-invalid, disabled sets aria-disabled and removes the thumbs from the tab order.
name, one entry per thumb.
Keyboard support#
| Key | Function |
|---|---|
step (in a right-to-left page ← increases). | |
step. | |