{"_id":"@agencecinq/slider","_rev":"2-26175f943e3ca2e9624fb1bade83afa8","name":"@agencecinq/slider","dist-tags":{"latest":"6.0.0"},"versions":{"6.0.0":{"name":"@agencecinq/slider","version":"6.0.0","author":{"url":"https://agencecinq.com","name":"Jérémy Levron","email":"jeremy@agencecinq.com"},"license":"MIT","_id":"@agencecinq/slider@6.0.0","maintainers":[{"name":"19h47","email":"jeremylevron@19h47.fr"}],"homepage":"https://github.com/agencecinq/shopify#readme","bugs":{"url":"https://github.com/agencecinq/shopify/issues"},"dist":{"shasum":"114b7619a24174219f058742258f93757722bd1c","tarball":"https://registry.npmjs.org/@agencecinq/slider/-/slider-6.0.0.tgz","fileCount":17,"integrity":"sha512-kr7Mrk9tDXkVpIXA37VR7PdOL7yWpPNRMMjSxmEcE5dQECTOk+L7KwhfoCIoyWcteSQZhfW4cb3RIgPf3Hn30w==","signatures":[{"sig":"MEQCIDM7anT1VegGOb4GX7y0UIjKuPHN3uHP2OWdXttzxFBXAiB+o5nfxyqjYJ2pdhJZ6e6i/yRe1bQx1qvo5EdEH8nXIw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":44788},"main":"./dist/index.js","type":"module","_from":"file:agencecinq-slider-6.0.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"vite build --watch","test":"vitest run","build":"vite build","test:watch":"vitest"},"_npmUser":{"name":"19h47","email":"jeremylevron@19h47.fr"},"_resolved":"/private/var/folders/vt/kpbgrh4d7k14zvxmcm01bfxh0000gn/T/9cd2173955a61c29e590236042fc09c7/agencecinq-slider-6.0.0.tgz","_integrity":"sha512-kr7Mrk9tDXkVpIXA37VR7PdOL7yWpPNRMMjSxmEcE5dQECTOk+L7KwhfoCIoyWcteSQZhfW4cb3RIgPf3Hn30w==","repository":{"url":"git+https://github.com/agencecinq/shopify.git","type":"git","directory":"packages/slider"},"_npmVersion":"11.19.1","description":"Accessible, WAI-ARIA slider as a lightweight Web Component.","directories":{},"_nodeVersion":"22.22.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.2.0","vitest":"^3.2.4","vite-plugin-dts":"^5.0.3","@agencecinq/utils":"7.2.0"},"peerDependencies":{"@agencecinq/utils":"*"},"_npmOperationalInternal":{"tmp":"tmp/slider_6.0.0_1788859621634_0.15416372005143875","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-09-08T09:27:01.457Z","modified":"2026-09-15T13:10:36.551Z","6.0.0":"2026-09-08T09:27:01.775Z"},"bugs":{"url":"https://github.com/agencecinq/shopify/issues"},"author":{"url":"https://agencecinq.com","name":"Jérémy Levron","email":"jeremy@agencecinq.com"},"license":"MIT","homepage":"https://github.com/agencecinq/shopify#readme","repository":{"url":"git+https://github.com/agencecinq/shopify.git","type":"git","directory":"packages/slider"},"description":"Accessible, WAI-ARIA slider as a lightweight Web Component.","maintainers":[{"email":"jeremylevron@19h47.fr","name":"19h47"},{"email":"pauline@agencecinq.com","name":"paulinecinq"}],"readme":"[![](https://img.shields.io/npm/v/@agencecinq/slider)](https://www.npmjs.com/package/@agencecinq/slider)\n[![](https://img.shields.io/npm/dm/@agencecinq/slider)](https://www.npmjs.com/package/@agencecinq/slider)\n\n# @agencecinq/slider\n\n> Accessible, WAI-ARIA slider as a lightweight Web Component.\n\nA slider lets users pick a value within a range by moving a thumb along a rail.\n`<cinq-slider>` is the **rail** (geometry + CSS variables). A nested\n`[role=\"slider\"]` thumb is the focusable control: ARIA bounds, labels, and\nkeyboard focus live there.\n\nImplementation follows the\n[WAI-ARIA Authoring Practices slider pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/).\nInspired by [`@19h47/slider`](https://github.com/19h47/19h47-slider).\n\nTwo custom elements in one package:\n\n| Tag | Pattern | Class |\n| --- | ------- | ----- |\n| `<cinq-slider>` | [Single-thumb slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) | `Slider` |\n| `<cinq-slider-range>` | [Multi-thumb range](https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/) | `SliderRange` |\n\nBoth support **horizontal** and **vertical** layout (`aria-orientation` on the thumb). Horizontal sliders mirror under RTL; vertical axis is unchanged by `dir`.\n\n## Installation\n\n```bash\npnpm add @agencecinq/slider\n```\n\n## Usage\n\n```html\n<label id=\"volume-label\" for=\"volume-slider\">Volume</label>\n<cinq-slider class=\"volume-rail\">\n  <button\n    type=\"button\"\n    id=\"volume-slider\"\n    role=\"slider\"\n    tabindex=\"0\"\n    aria-labelledby=\"volume-label\"\n    aria-orientation=\"horizontal\"\n    aria-valuemin=\"0\"\n    aria-valuemax=\"100\"\n    aria-valuenow=\"50\"\n    aria-valuetext=\"50%\"\n  ></button>\n</cinq-slider>\n```\n\n```js\nimport \"@agencecinq/slider\";\n```\n\nImporting `@agencecinq/slider` registers **both** custom elements automatically.\nNo manual `init()` call required.\n\n> **HTML is the source of truth.** The component will not auto-set `role`,\n> auto-migrate attributes, or warn about missing labels. Use an a11y linter\n> (axe-core, Lighthouse) to catch invalid markup.\n\nThe package implements APG behaviour (keyboard, pointer, ARIA sync, events).\n**Layout, hit area, contrast, and focus rings are your CSS.** See\n[Consumer styling](#consumer-styling) below.\n\n### Required markup, single (`<cinq-slider>`)\n\nAuthor contract (not enforced in JS, use an a11y linter). The component **does** throw if thumb count ≠ 1.\n\n| Attribute / element | Expected | Role |\n| ------------------- | -------- | ---- |\n| `<cinq-slider>` | **Yes** | Rail / bounds container. |\n| One `[role=\"slider\"]` | **Yes** | Focusable thumb (`$thumb`). |\n| `tabindex=\"0\"` | **Yes** | On the thumb. |\n| `aria-orientation=\"horizontal\"` or `\"vertical\"` | **Yes** | On the thumb. Geometry follows the declared axis. |\n| `aria-valuemin` / `aria-valuemax` / `aria-valuenow` | **Yes** | Read with fallbacks (`0` / `100`) if omitted. |\n| `aria-label` or `aria-labelledby` | **Yes** | On the thumb, per APG. |\n| `aria-valuetext` | Recommended | Updated by the component when `formatValue` is set. |\n\nPrefer `<button type=\"button\">` for a clear focus target.\n\n### Range (`<cinq-slider-range>`)\n\nAPG order: **first thumb in DOM = max handle**, **second = min handle**.\n\n```html\n<label id=\"price-label\">Price range</label>\n<cinq-slider-range class=\"price-rail\" aria-labelledby=\"price-label\">\n  <button\n    type=\"button\"\n    role=\"slider\"\n    tabindex=\"0\"\n    aria-label=\"Maximum price\"\n    aria-orientation=\"horizontal\"\n    aria-valuemin=\"100\"\n    aria-valuemax=\"1000\"\n    aria-valuenow=\"800\"\n  ></button>\n  <button\n    type=\"button\"\n    role=\"slider\"\n    tabindex=\"0\"\n    aria-label=\"Minimum price\"\n    aria-orientation=\"horizontal\"\n    aria-valuemin=\"100\"\n    aria-valuemax=\"1000\"\n    aria-valuenow=\"200\"\n  ></button>\n</cinq-slider-range>\n```\n\nRange track fill between thumbs:\n\n```css\ncinq-slider-range::after {\n  inset-inline-start: calc(\n    var(--thumb-size) / 2 + var(--min-ratio) * (100% - var(--thumb-size))\n  );\n  width: calc(\n    (var(--max-ratio) - var(--min-ratio)) * (100% - var(--thumb-size))\n  );\n  border-radius: 0; /* square ends between two handles */\n}\n```\n\nSee [Positioning with CSS variables](#positioning-with-css-variables) for the full pattern (rail, fill, thumbs).\n\n### Options\n\n| Attribute | Type | Default | Description |\n| --------- | ---- | ------- | ----------- |\n| `data-step` | number | `1` | Arrow key increment; values snap to this step (pointer + keyboard). |\n| `data-page` | number | `10` | Page Up / Page Down increment. |\n\nThumb and track layout are **consumer CSS**. The component only updates CSS custom properties on the host (see [Styling hooks](#styling-hooks)).\n\n### API, `<cinq-slider>` (`Slider`)\n\n| Method | Description |\n| ------ | ----------- |\n| `setValue(value, options?)` | Clamp, sync, dispatch unless `emit: false`. |\n| `sync()` | Re-read ARIA and re-apply geometry. |\n| `rect()` | Host `DOMRect` (rail). |\n| `init()` / `destroy()` | Bind / unbind for DOM mutation. |\n\n| Property | Description |\n| -------- | ----------- |\n| `$thumb` | The `[role=\"slider\"]` element. |\n| `value` / `min` / `max` / `ratio` | From thumb ARIA. |\n| `step` / `page` | From host `data-step` / `data-page`. |\n| `formatValue` | `(value) => string` for `aria-valuetext`. |\n\n### API, `<cinq-slider-range>` (`SliderRange`)\n\n| Method | Description |\n| ------ | ----------- |\n| `setValues(min, max, options?)` | Anti-cross clamp, sync, dispatch unless `emit: false`. |\n| `sync()` | Re-read ARIA and re-apply geometry. |\n| `rect()` | Host `DOMRect` (rail). |\n| `init()` / `destroy()` | Bind / unbind for DOM mutation. |\n\n| Property | Description |\n| -------- | ----------- |\n| `$max` / `$min` | Thumbs (first / second in DOM). |\n| `min` / `max` | Current selected values. |\n| `boundsMin` / `boundsMax` | Global bounds from initial markup. |\n| `step` / `page` | From host `data-step` / `data-page`. |\n| `formatValue` | `(value) => string` for both thumbs. |\n\n### Keyboard\n\nMatches [APG slider keyboard interaction](https://www.w3.org/WAI/ARIA/apg/patterns/slider/#keyboard-interaction).\nFocus must be on the thumb.\n\n**Range (`<cinq-slider-range>`):** both thumbs are in the tab sequence (APG\nmulti-thumb). Use **Tab** / **Shift+Tab** to move focus between the max handle\n(first in DOM) and the min handle (second). Arrow keys adjust the **focused**\nthumb only. Tab order follows DOM order, not visual position when handles\ncross. Style `:focus-visible` and raise `z-index` on the focused thumb so the\nring stays visible (see playground `[data-active]` pattern).\n\n**Vertical:** Up Arrow increases, Down Arrow decreases. **RTL does not apply**\nto the vertical axis (same as `@19h47/slider`).\n\n| Key | Horizontal LTR | Horizontal RTL | Vertical |\n| --- | -------------- | -------------- | -------- |\n| Right Arrow | + `data-step` | − `data-step` | + `data-step` |\n| Left Arrow | − `data-step` | + `data-step` | − `data-step` |\n| Up Arrow | + `data-step` | + `data-step` | + `data-step` |\n| Down Arrow | − `data-step` | − `data-step` | − `data-step` |\n| Home / End | min / max (single); APG multi-thumb rules (range) | same | same |\n| Page Up / Down | + / − `data-page` | same | same |\n\n### RTL\n\nSet `dir=\"rtl\"` on the host or an ancestor for **horizontal** sliders. Thumb\nposition, pointer mapping, and horizontal arrow keys mirror (same as\n`@agencecinq/tabs`). Vertical sliders ignore `direction` on the value axis.\n\n### Events\n\nBoth hosts fire `slider:change` (constant `EVENTS.SLIDER_CHANGE`) after ARIA sync.\n\nDetail: `{ min, max, $thumb }`. Single thumb: `min === max`. Range: current\nendpoints. `$thumb` is always the handle that moved or has focus.\n\n```js\nimport { EVENTS } from \"@agencecinq/utils\";\n\ndocument.querySelector(\"cinq-slider\")?.addEventListener(\n  EVENTS.SLIDER_CHANGE,\n  (event) => {\n    const { min, max, $thumb } = event.detail;\n    console.log(min, max, $thumb);\n  },\n);\n\ndocument.querySelector(\"cinq-slider-range\")?.addEventListener(\n  EVENTS.SLIDER_CHANGE,\n  (event) => {\n    const { min, max, $thumb } = event.detail;\n    const range = event.currentTarget;\n    const which =\n      $thumb === range.$min ? \"min\" : $thumb === range.$max ? \"max\" : null;\n    console.log(min, max, which);\n  },\n);\n```\n\n### Styling hooks\n\n| Hook | Host | Description |\n| ---- | ---- | ----------- |\n| `[dragging]` | Both | Pointer drag in progress |\n| `--value` / `--ratio` | `<cinq-slider>` | Current value and semantic ratio `0` to `1` |\n| `--min` / `--max` | `<cinq-slider-range>` | Current range values |\n| `--min-ratio` / `--max-ratio` | `<cinq-slider-range>` | Semantic ratios `0` to `1` within global bounds |\n| `[data-active=\"min\"\\|\"max\"]` | `<cinq-slider-range>` | Last focused / dragged thumb |\n| `[data-collapsed]` | `<cinq-slider-range>` | Set when `min === max` (side-by-side thumb CSS) |\n\nRatios are **semantic**: `0` = global minimum, `1` = global maximum, unchanged in RTL. Use **logical properties** (`inset-inline-start`, etc.) so layout mirrors with `dir=\"rtl\"`.\n\n### Positioning with CSS variables\n\nThe package never positions thumbs or track in JS. It updates ARIA and sets the variables above on the **host**. You style the rail, fill, and thumbs in your stylesheet.\n\nDefine a thumb size on the host (name is yours, the lib does not set it):\n\n```css\ncinq-slider,\ncinq-slider-range {\n  position: relative;\n  --thumb-size: 2.75rem; /* hit area width/height of [role=\"slider\"] */\n}\n```\n\nThe usable track is shorter than the host by one thumb (single) or two thumbs (range). Inset the rail and map ratios over `(100% - var(--thumb-size))` (or `2 * var(--thumb-size)` for range).\n\n#### Single thumb (`<cinq-slider>`)\n\n**Variables:** `--value` (number), `--ratio` (`0` to `1`).\n\n**Track rail** (full width, gray):\n\n```css\ncinq-slider::before {\n  content: \"\";\n  position: absolute;\n  inset-inline: calc(var(--thumb-size) / 2);\n  top: 50%;\n  block-size: 0.375rem;\n  border-radius: 9999px;\n  background: #e5e7eb;\n  transform: translateY(-50%);\n  pointer-events: none;\n}\n```\n\n**Track fill** (from start to current value):\n\n```css\ncinq-slider::after {\n  content: \"\";\n  position: absolute;\n  inset-inline-start: calc(var(--thumb-size) / 2);\n  top: 50%;\n  width: calc(var(--ratio) * (100% - var(--thumb-size)));\n  block-size: 0.375rem;\n  border-radius: 9999px;\n  background: #111827;\n  transform: translateY(-50%);\n  pointer-events: none;\n}\n```\n\n**Thumb** (centered on ratio):\n\n```css\ncinq-slider [role=\"slider\"] {\n  position: absolute;\n  top: calc(50% - var(--thumb-size) / 2);\n  width: var(--thumb-size);\n  height: var(--thumb-size);\n  inset-inline-start: calc(var(--ratio) * (100% - var(--thumb-size)));\n}\n```\n\nAlternative: keep `inset-inline-start: 0` and use `translate: calc(var(--ratio) * (100% - var(--thumb-size))) 0`.\n\n#### Range (`<cinq-slider-range>`)\n\n**Variables:** `--min`, `--max`, `--min-ratio`, `--max-ratio`.\n\nAPG DOM order: **`:first-child` = max handle**, **`:last-child` = min handle**.\n\n**Track fill** (segment between the two values):\n\n```css\ncinq-slider-range::after {\n  content: \"\";\n  position: absolute;\n  inset-inline-start: calc(\n    var(--thumb-size) / 2 + var(--min-ratio) * (100% - var(--thumb-size))\n  );\n  top: 50%;\n  width: calc(\n    (var(--max-ratio) - var(--min-ratio)) * (100% - var(--thumb-size))\n  );\n  block-size: 0.375rem;\n  border-radius: 0;\n  background: #111827;\n  transform: translateY(-50%);\n  pointer-events: none;\n}\n```\n\n**Thumbs:**\n\n```css\ncinq-slider-range [role=\"slider\"]:first-child {\n  inset-inline-start: calc(var(--max-ratio) * (100% - var(--thumb-size)));\n  z-index: 2;\n}\n\ncinq-slider-range [role=\"slider\"]:last-child {\n  inset-inline-start: calc(var(--min-ratio) * (100% - var(--thumb-size)));\n}\n```\n\n**Collapsed range** (`min === max`): the host gets `[data-collapsed]`. Offset the min\nhandle by one thumb width on the low side so both handles stay visible and\nselectable (consumer CSS):\n\n```css\ncinq-slider-range[data-collapsed] [role=\"slider\"]:last-child {\n  inset-inline-start: calc(\n    var(--min-ratio) * (100% - var(--thumb-size)) - var(--thumb-size)\n  );\n}\n\n[dir=\"rtl\"] cinq-slider-range[data-collapsed] [role=\"slider\"]:last-child {\n  inset-inline-start: calc(\n    var(--min-ratio) * (100% - var(--thumb-size)) + var(--thumb-size)\n  );\n}\n\ncinq-slider-range:has([aria-orientation=\"vertical\"])[data-collapsed] [role=\"slider\"]:last-child {\n  inset-block-end: calc(\n    var(--min-ratio) * (100% - var(--thumb-size)) - var(--thumb-size)\n  );\n}\n```\n\nWhen handles differ, raise the active thumb with `[data-active]`:\n\n```css\ncinq-slider-range[data-active=\"min\"] [role=\"slider\"]:last-child,\ncinq-slider-range[data-active=\"max\"] [role=\"slider\"]:first-child {\n  z-index: 3;\n}\n```\n\n#### Vertical, `<cinq-slider>`\n\nSemantic `--ratio` is unchanged (`0` = min, `1` = max). Map along the block\naxis with min at the bottom and max at the top (see **Potion potency** in the playground):\n\n```css\ncinq-slider:has([aria-orientation=\"vertical\"]) {\n  width: var(--thumb-size);\n  height: 12rem;\n}\n\ncinq-slider:has([aria-orientation=\"vertical\"])::before {\n  inset-block: calc(var(--thumb-size) / 2);\n  inset-inline-start: 50%;\n  inline-size: 0.375rem;\n  transform: translateX(-50%);\n}\n\ncinq-slider:has([aria-orientation=\"vertical\"])::after {\n  inset-inline-start: 50%;\n  inset-block-end: calc(var(--thumb-size) / 2);\n  block-size: calc(var(--ratio) * (100% - var(--thumb-size)));\n  inline-size: 0.375rem;\n  transform: translateX(-50%);\n}\n\ncinq-slider [aria-orientation=\"vertical\"] {\n  inset-inline-start: calc(50% - var(--thumb-size) / 2);\n  inset-block-end: calc(var(--ratio) * (100% - var(--thumb-size)));\n}\n```\n\nRange vertical thumbs and fill use `--min-ratio` / `--max-ratio` on\n`inset-block-end` the same way. See the docs playground **HP comfort band** demo.\n\n#### Interaction hooks\n\n```css\ncinq-slider[dragging] [role=\"slider\"],\ncinq-slider-range[dragging] [role=\"slider\"] {\n  cursor: grabbing;\n}\n\ncinq-slider [role=\"slider\"]:focus-visible,\ncinq-slider-range [role=\"slider\"]:focus-visible {\n  outline: 2px solid currentColor;\n  outline-offset: 3px;\n}\n```\n\nLead offsets for multi-thumb pointer mapping follow `@19h47/slider` (horizontal:\nmax thumb; vertical: min thumb). The [interactive docs](https://agencecinq.github.io/ui/components/slider/) playground is the reference implementation.\n\n### Consumer styling\n\nThe component does **not** enforce thumb size, contrast, or touch targets.\n\n- **Touch targets:** WCAG recommends at least **44×44 CSS px** for pointer\n  targets. You can use a large hit area with a smaller visual knob (`::before`).\n- **Focus:** use `:focus-visible` on the thumb.\n- **Touch AT:** the APG [warns](https://www.w3.org/WAI/ARIA/apg/patterns/slider/)\n  that some touch-based assistive technologies may not yet synthesize the key\n  events custom sliders need. Test with VoiceOver / TalkBack on real devices.\n\n### `formatValue`\n\n```js\nconst host = document.querySelector(\"cinq-slider\");\nhost.formatValue = (value) => `${value}%`;\n```\n\n### Runtime DOM mutation\n\n```js\nhost.destroy();\n// mutate light DOM...\nhost.init();\n```\n\n## Build setup\n\n```bash\npnpm -C packages/slider build\n```\n\n## Acknowledgments\n\n- [WAI-ARIA Slider Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/)\n- [`@19h47/slider`](https://github.com/19h47/19h47-slider)\n- [Interactive docs](https://agencecinq.github.io/ui/components/slider/)\n","readmeFilename":"README.md"}