On this page
Choices

Slider released

A position on a numeric continuum, dragged with the pointer or walked with the arrows.

  • Import@pacit/components/slider
  • Selectorpct-slider

Implements the W3C ARIA APG Slider pattern ↗

At 40 of 100.

slider.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctSlider } from '@pacit/components/slider';
ExportedWhat 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-sliderSlider. 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" />
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

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.

Seats on the plan

10 seats.

API

What it reads, and what it writes back

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).

Inputs

NameTypeWhat it does
valuetwo-waydefault 0numberThe 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 falsebooleanBlocks 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 falsebooleanA 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 falsebooleanThe form's verdict; shown only once touched, so an empty form does not open red.
toucheddefault falsebooleanWhether the user has left the field once; with invalid it gates the error face.
requireddefault falsebooleanMarks 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 undefinedreadonly ValidationError.WithOptionalFieldTree[] | undefinedA 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 ''stringThe native name — what a form submission calls the value.
mindefault undefinedunknownValue 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 undefinedunknownThe upper bound — as min: the form's and the native max in one; 100 when absent.
stepdefault 1numberThe granularity — and the only snapping there is (<datalist> snaps in no engine).
labeldefault ''stringThe visible label, rendered by the control itself when it stands outside a pct-field; inside one, the field's label is the name.
hintdefault ''stringA 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 ''stringThe 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 ''stringAs ariaLabel, for a name that already stands somewhere on the page.
formatdefault nullIntl.NumberFormatOptions | nullHow 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 falsebooleanTicks at every step. Our own drawing — <datalist> renders none in firefox.
orientationdefault 'horizontal'PctSliderOrientationWhich axis the slider runs along. Vertical is writing-mode, so no rule reads the direction.
localedefault ''stringOverrides the application's LOCALE_ID for the formatted value, as on [pctNumber].

Outputs

NamePayloadWhen it fires
touchvoidEmitted on blur — lets the form mark the field as touched.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-slider
data-pct-orientationorientation()
data-pct-invalidshowInvalid() ? "" : null
data-pct-warningwarningLit() ? "" : null
data-pct-disableddisabled() ? "" : null
data-pct-in-fieldinField ? "" : null
[style.--_pct-slider-fraction]propertyfraction()
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

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.

PartSelectorWhat 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)

Tokens

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.

TokenStyles
--pct-slider-bubble-bg{pct.surface-100}#f1f5f9 · #1e293bBackground of the value bubble.
--pct-slider-bubble-fg{pct.text}#0f172a · #ffffffText colour of the value bubble.
--pct-slider-bubble-font-size{pct.font.size.sm}13pxFont size of the value bubble.
--pct-slider-bubble-padding-x{pct.space.2}4pxHorizontal padding of the value bubble.
--pct-slider-bubble-radius{pct.radius.md}8pxCorner radius of the value bubble.
--pct-slider-fg-disabled{pct.text-disabled}#64748bText colour of the slider while disabled.
--pct-slider-fg-invalid{pct.danger}#b91c1c · #f87171Text colour of the slider when invalid.
--pct-slider-fg-warning{pct.warning}#b45309 · #fbbf24Text 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 · #60a5faBackground of the fill.
--pct-slider-fill-bg-disabled{pct.text-disabled}#64748bBackground of the fill while disabled.
--pct-slider-gap{pct.space.3}8pxGap between the slider and its label.
--pct-slider-height10remHeight of the slider.
--pct-slider-hint-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the hint.
--pct-slider-label-fg{pct.text}#0f172a · #ffffffText colour of the label.
--pct-slider-mark-bg{pct.text-muted}#334155 · #e2e8f0Background of the mark.
--pct-slider-mark-size2pxSize of the mark.
--pct-slider-target-min{pct.target.min}24pxTouch-target floor of the slider.
--pct-slider-thumb-bg{pct.on-primary}#ffffff · #0f172aBackground of the thumb.
--pct-slider-thumb-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293bBackground of the thumb while disabled.
--pct-slider-thumb-border{pct.primary}#2563eb · #60a5faEdge colour of the thumb.
--pct-slider-thumb-border-disabled{pct.border}#e2e8f0 · #334155Edge colour of the thumb while disabled.
--pct-slider-thumb-border-hover{pct.primary-hover}#1d4ed8 · #93c5fdEdge colour of the thumb on hover.
--pct-slider-thumb-border-invalid{pct.danger}#b91c1c · #f87171Edge colour of the thumb when invalid.
--pct-slider-thumb-size18pxSize of the thumb.
--pct-slider-track-bg{pct.border}#e2e8f0 · #334155Background of the track.
--pct-slider-track-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293bBackground of the track while disabled.
--pct-slider-track-height6pxHeight of the track.
--pct-slider-track-radius999pxCorner radius of the track.

Theming

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.

Accessibility

Measured, not assumed

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

Keyboard

keyeffecttest
ArrowRight / ArrowUpone step upnative <input type="range">
ArrowLeft / ArrowDownone step downnative
PageUp / PageDownten stepsnative
Home / Endmin / maxnative

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.

What is measured, and what is not yet

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.

MeasuredARIA APG pattern in the JSDoclibs/components/slider/src/slider.ts — the pattern is named and linked in the class comment
MeasuredKeyboard mapapps/sandbox-e2e/src/slider.spec.ts — arrows, PageUp/PageDown, Home/End, in three engines
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts — the /slider view and /all, in both directions
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — slider-states and slider-states-rtl
Measuredforced-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 rest
n/aprefers-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 off
MeasuredTouch target ≥ 24×24 pxapps/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 sees
n/aSize axisnot applicable — the bare variant does not align heights (0004)
GapDensity axisnone — gap. Elevated risk: the row is exactly at the touch threshold, so density has nowhere to go down
MeasuredRTLapps/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 baseline
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts — the /slider view is in SBX_ROUTES
MeasuredFormslibs/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.ts
MeasuredMessage announcedtools/check-aria.mjs (point 7, target check-aria) — the error part carries role="alert"
MeasuredParts in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts)
MeasuredTokens + contrast.policy.jsonlibs/tokens/src/contrast.policy.json — 14 pairs, bubble / fill / mark added to the name dictionary
MeasuredStrings through PCT_TEXTStools/check-texts.mjs — no strings of its own; the value in words is Intl.NumberFormat or the consumer's labels
MeasuredSize budgetlibs/components/size.snapshot.md, tools/check-bundle.mjs (target check-bundle)
MeasuredScreen-reader logRead 2026-09-28 from 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
Measureddocs page/components/slider on the published site — prerendered, the demo's own source is the code tab
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

80.11%mutation score149 of 186 mutants killed
10 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
16colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
20elements in the preview3 listeners · 1 render to settle · 4826 µs in jsdom, held exactly but for the clock

Decisions behind it

  • 0042A slider is the platform's range, and only the value it cannot pronounce is ours
  • 0039A state the platform publishes is not ours to write
  • 0034Multiplicity is a type, so it is a tag
  • 0005Signal forms without ControlValueAccessor
  • 0003Wrapper and control
  • lesson-112An ARIA attribute over a native state is written by us and read by nobody

Known limitations

  • One thumb. A min–max range forks the value to [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.
  • The marks are hidden behind the fill. See above: the contrast pair is what decides it, and a tick that reappears when the thumb moves back is the price.
  • No <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.