A range of its own, with marks
min, max and step set the scale; marks draws a tick per step, so a slider with five stops reads as five stops before the thumb moves.
10 seats.
A position on a numeric continuum, dragged with the pointer or walked with the arrows.
@pacit/components/sliderpct-sliderImplements the W3C ARIA APG Slider pattern ↗
At 40 of 100.
import { Component, signal } from '@angular/core';
import { PctSlider } from '@pacit/components/slider';
/** A native range under the paint — steps, marks and the announced value for free. */
@Component({
selector: 'demo-slider',
imports: [PctSlider],
styles: ':host { display: block; max-inline-size: 24rem; }',
template: `
<pct-slider label="Volume" [(value)]="volume" [step]="10" [marks]="true" />
<p>At {{ volume() }} of 100.</p>
`,
})
export class SliderDemo {
readonly volume = signal(40);
}slider.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctSlider } from '@pacit/components/slider';| Exported | What it is |
|---|---|
PctSliderOrientationtype'horizontal' | 'vertical' | Which way the slider runs. An input rather than a tag — the value type is a number in both (0042). |
PctSlidercomponentpct-slider | Slider. A position on a numeric continuum, drawn on the platform's own <input type="range"> — the ARIA APG's Slider pattern, in the variant the APG's own HTML example is: the native element, which carries the role, the value, the bounds and the whole keyboard without a line from us (0042). |
<pct-slider label="Volume" [(value)]="volume" />Each example is the component itself, not a screenshot. Open the code under any of them.
min, max and step set the scale; marks draws a tick per step, so a slider with five stops reads as five stops before the thumb moves.
10 seats.
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
unreleased marks 1 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.
Slider. A position on a numeric continuum, drawn on the platform's own <input type="range"> — the ARIA APG's Slider pattern, in the variant the APG's own HTML example is: the native element, which carries the role, the value, the bounds and the whole keyboard without a line from us (0042).
| Name | Type | What it does |
|---|---|---|
valuetwo-waydefault 0 | number | The position. A number and never a T: a slider is a place on a numeric continuum, and a control with named steps is that same number with a labels list over it, so req-api-generic is met by NOT being generic. |
disableddefault false | boolean | Blocks the control and greys it — the native disabled, so it leaves the tab order as well. With [formField] the directive writes it, as it writes every input of the FormUiControl contract. |
readonlydefault false | boolean | A native range has no readonly, so a move is undone here instead; the thumb stays focusable, unlike under disabled, and aria-readonly says so. |
invaliddefault false | boolean | The form's verdict; shown only once touched, so an empty form does not open red. |
toucheddefault false | boolean | Whether the user has left the field once; with invalid it gates the error face. |
requireddefault false | boolean | Marks the label with the required sign; with [formField] it follows the schema's required(). |
errorsdefault [] | readonly ValidationError.WithOptionalFieldTree[] | The form's validation errors; the first one's message takes the hint's place once the field is touched. |
warningsunreleaseddefault undefined | readonly ValidationError.WithOptionalFieldTree[] | undefined | A verdict without a veto (0087): shown after the error and before the hint once the field is touched, in the warning tone, with aria-invalid untouched. Left unbound, the control reads what pctWarn() wrote for its own [formField]; bound — [] included — the list given stands. |
namedefault '' | string | The native name — what a form submission calls the value. |
mindefault undefined | unknown | Value bounds. They belong to the FormUiControl contract, so with [formField] the directive fills them itself from the schema's min() / max() validators — and here they are also the native min / max attributes, so one pair of numbers is both what the form validates and what the platform clamps to. |
maxdefault undefined | unknown | The upper bound — as min: the form's and the native max in one; 100 when absent. |
stepdefault 1 | number | The granularity — and the only snapping there is (<datalist> snaps in no engine). |
labeldefault '' | string | The visible label, rendered by the control itself when it stands outside a pct-field; inside one, the field's label is the name. |
hintdefault '' | string | A line of help under the control, outside a pct-field; the first error message takes its place while the field is invalid and touched. |
ariaLabeldefault '' | string | The accessible name of a slider with no visible label — an INPUT rather than an aria-label on the tag, for the same reason as on the switch: the role sits on the <input> inside and an ARIA name on the roleless host is ignored. |
ariaLabelledbydefault '' | string | As ariaLabel, for a name that already stands somewhere on the page. |
formatdefault null | Intl.NumberFormatOptions | null | How the value is SAID — Intl.NumberFormat options. Given, it turns on the visible bubble and aria-valuetext; absent, the platform's bare aria-valuenow is the whole announcement, which is what a plain 0–100 slider wants. |
labelsdefault [] | readonly string[] | Named steps ("Small", "Medium", "Large"), indexed from min by step. It wins over format: a name is what the reader gets and the number underneath is the value. |
marksdefault false | boolean | Ticks at every step. Our own drawing — <datalist> renders none in firefox. |
orientationdefault 'horizontal' | PctSliderOrientation | Which axis the slider runs along. Vertical is writing-mode, so no rule reads the direction. |
localedefault '' | string | Overrides the application's LOCALE_ID for the formatted value, as on [pctNumber]. |
| Name | Payload | When it fires |
|---|---|---|
touch | void | Emitted on blur — lets the form mark the field as touched. |
The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-slider | |
data-pct-orientation | orientation() | |
data-pct-invalid | showInvalid() ? "" : null | |
data-pct-warning | warningLit() ? "" : null | |
data-pct-disabled | disabled() ? "" : null | |
data-pct-in-field | inField ? "" : null | |
[style.--_pct-slider-fraction]property | fraction() |
Two ways in, both stable. No class names to depend on.
In a test, PctSliderHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.
| Part | Selector | What it is |
|---|---|---|
bubble | [data-pct-part="bubble"] | the value shown while dragging |
control | [data-pct-part="control"] | the native range input |
error | [data-pct-part="error"] | the message when invalid |
fill | [data-pct-part="fill"] | the part up to the thumb |
hint | [data-pct-part="hint"] | the hint under the control |
label | [data-pct-part="label"] | the label of the control |
mark | [data-pct-part="mark"] | one tick along the track |
thumb | [data-pct-part="thumb"] | the handle |
track | [data-pct-part="track"] | the full length |
warning | [data-pct-part="warning"] | the message when the value is allowed and suspect — after the error, before the hint (0087) |
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
| Token | Styles |
|---|---|
--pct-slider-bubble-bg{pct.surface-100}#f1f5f9 · #1e293b | Background of the value bubble. |
--pct-slider-bubble-fg{pct.text}#0f172a · #ffffff | Text colour of the value bubble. |
--pct-slider-bubble-font-size{pct.font.size.sm}13px | Font size of the value bubble. |
--pct-slider-bubble-padding-x{pct.space.2}4px | Horizontal padding of the value bubble. |
--pct-slider-bubble-radius{pct.radius.md}8px | Corner radius of the value bubble. |
--pct-slider-fg-disabled{pct.text-disabled}#64748b | Text colour of the slider while disabled. |
--pct-slider-fg-invalid{pct.danger}#b91c1c · #f87171 | Text colour of the slider when invalid. |
--pct-slider-fg-warning{pct.warning}#b45309 · #fbbf24 | Text colour of the warning line (0087); the thumb keeps its colour, a warning being no fault of the control. |
--pct-slider-fill-bg{pct.primary}#2563eb · #60a5fa | Background of the fill. |
--pct-slider-fill-bg-disabled{pct.text-disabled}#64748b | Background of the fill while disabled. |
--pct-slider-gap{pct.space.3}8px | Gap between the slider and its label. |
--pct-slider-height10rem | Height of the slider. |
--pct-slider-hint-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the hint. |
--pct-slider-label-fg{pct.text}#0f172a · #ffffff | Text colour of the label. |
--pct-slider-mark-bg{pct.text-muted}#334155 · #e2e8f0 | Background of the mark. |
--pct-slider-mark-size2px | Size of the mark. |
--pct-slider-target-min{pct.target.min}24px | Touch-target floor of the slider. |
--pct-slider-thumb-bg{pct.on-primary}#ffffff · #0f172a | Background of the thumb. |
--pct-slider-thumb-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293b | Background of the thumb while disabled. |
--pct-slider-thumb-border{pct.primary}#2563eb · #60a5fa | Edge colour of the thumb. |
--pct-slider-thumb-border-disabled{pct.border}#e2e8f0 · #334155 | Edge colour of the thumb while disabled. |
--pct-slider-thumb-border-hover{pct.primary-hover}#1d4ed8 · #93c5fd | Edge colour of the thumb on hover. |
--pct-slider-thumb-border-invalid{pct.danger}#b91c1c · #f87171 | Edge colour of the thumb when invalid. |
--pct-slider-thumb-size18px | Size of the thumb. |
--pct-slider-track-bg{pct.border}#e2e8f0 · #334155 | Background of the track. |
--pct-slider-track-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293b | Background of the track while disabled. |
--pct-slider-track-height6px | Height of the track. |
--pct-slider-track-radius999px | Corner radius of the track. |
A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.
[data-theme='brand'] {
--pct-slider-fill-bg: #0f766e;
--pct-slider-thumb-bg: #0f766e;
--pct-slider-bubble-bg: #134e4a;
--pct-slider-bubble-fg: #ffffff;
}At 40 of 100.
Pattern: Slider — in the variant the APG's own HTML example is: a native <input type="range">, which carries the role, the value, both bounds and the whole keyboard without a line from us
| key | effect | test |
|---|---|---|
ArrowRight / ArrowUp | one step up | native <input type="range"> |
ArrowLeft / ArrowDown | one step down | native |
PageUp / PageDown | ten steps | native |
Home / End | min / max | native |
No handling of its own (req-api-platform) — the whole map is the platform's and it is byte-identical in three engines, which is a claim about three of them and so measured in three. readonly blocks the change without losing focusability; disabled drops the control from the Tab order, which is again the platform's doing.
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
libs/components/slider/src/slider.ts — the pattern is named and linked in the class commentapps/sandbox-e2e/src/slider.spec.ts — arrows, PageUp/PageDown, Home/End, in three enginesapps/sandbox-e2e/src/a11y.spec.ts — the /slider view and /all, in both directionsapps/sandbox-e2e/src/visual.spec.ts — slider-states and slider-states-rtlforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts — the slider repaints more than the others, because appearance: none is what lets a range be drawn at all and it takes the platform's own high-contrast rendering with it; the disabled row says GrayText like the restprefers-reduced-motionnot applicable — the slider has no transition at all. A thumb that eased into place would lag the finger dragging it, so the position is written outright and there is nothing for the preference to switch offapps/sandbox-e2e/src/slider.spec.ts — the floor is on the ROW (max(--pct-slider-thumb-size, --pct-slider-target-min)), and the row is the native input, so what a finger gets is what an eye seesapps/sandbox-e2e/src/slider.spec.ts — the drawing mirrors as geometry AND the platform's own pointer reading mirrors with it, plus the slider-states-rtl baselineapps/sandbox-e2e/src/hydration.spec.ts — the /slider view is in SBX_ROUTESlibs/components/slider/src/slider.spec.ts — signal forms (bounds from the schema's validators), [formControl] and [(ngModel)]; a form's reset — reset() keeping the value, reset(value) showing it, the form pristine: libs/components/slider/src/slider.spec.tstools/check-aria.mjs (point 7, target check-aria) — the error part carries role="alert"libs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts)contrast.policy.jsonlibs/tokens/src/contrast.policy.json — 14 pairs, bubble / fill / mark added to the name dictionaryPCT_TEXTStools/check-texts.mjs — no strings of its own; the value in words is Intl.NumberFormat or the consumer's labelslibs/components/size.snapshot.md, tools/check-bundle.mjs (target check-bundle)docs/acr/at/ — three readers, whole sandbox, and aria-valuetext reaches the readers rather than chromium's tree alone: the formatted slider is announced Discount · slider · 15 % · 0 percent. (Orca), Discount, slider, 15 percent (NVDA); the one whose value is a word gives Size · slider · Medium, Size, slider, Medium. VoiceOver reached the first slider only in this pass — 40 Budget Between 20 and 80 slider: value, label, hint, role — and had read 15 % Discount slider and Medium Size slider on 2026-09-16 (543eacde). Orca alone adds a raw percentage of its own beside the text, which is its habit and not this component/components/slider on the published site — prerendered, the demo's own source is the code tabRead at build time from the tracked snapshots, not typed here.
ControlValueAccessor[number, number], which is a question about the tag rather than about an input, and two overlaid native ranges do not compose — a press at the lower thumb moves the upper input in all three engines. A consumer who needs it today composes two PctSliders and inherits that defect.<datalist>. It draws no tick in firefox and snaps in no engine, so a consumer who passes one gets the browser's own behaviour and nothing of ours.aria-valuetext is measured in one engine. Playwright 1.61 reports valuenow and not valuetext, so the accessibility-tree reading is chromium's CDP; the visible bubble is what carries the value on screen in the other two.