Preview
Installation
Install the runtime packages:
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives
Or scaffold the component source directly into your codebase (shadcn-style):
pnpm dlx @oshon-ai/cli add slider
Wire the tokens into your Tailwind v4 stylesheet:
/* app/globals.css */ @import 'tailwindcss'; @import '@oshon-ai/tokens/css'; @import '@oshon-ai/tokens/tailwind';
New here? Walk through the full setup — prereqs, theming, your first render.
Usage
Import the component and render it. Every component supports the standard tier, size, and disabled props where applicable.
'use client';
import { Slider } from '@oshon-ai/components';
export default function Example() {
return <Slider />;
}Styling
Three layers of customization, in order of escape-hatch strength: className overrides → data-attribute targeting → CSS custom properties.
Passing Tailwind classes
Every Oshon component accepts a className prop merged AFTER the component's default classes. Use it to override spacing, color, or size without forking the component.
<Slider className="ring-2 ring-offset-2 ring-blue-500" />
Data attributes
Oshon components expose their internal state as data-oshon-* attributes so you can target them from CSS without coupling to internal class names. The most common attributes are listed below — see the component's source for the full set.
| Attribute | Values | Description |
|---|---|---|
data-oshon-size | xs · s · m · l · mobile | Visual size axis. Mirrors the `size` prop. |
data-oshon-tier | primary · secondary · tertiary | Visual emphasis tier (Button family). Mirrors the `tier` prop. |
data-oshon-state | enabled · active · error · disabled | Component surface state. Set automatically based on props. |
data-disabled | true · (omitted) | Set when `disabled` is true. Pair with `:disabled` CSS for native input components. |
data-state | open · closed · checked · unchecked · … | Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.). |
/* Target the secondary tier specifically */
[data-oshon-tier="secondary"] {
--oshon-color-primary-700: var(--my-brand-color);
}Interactive states
Every interactive component supports the standard CSS pseudo- classes plus Tailwind's state variants. Focus rings always use :focus-visible so keyboard users see them but mouse users don't.
:hover/hover:*— pointer hover:focus-visible/focus-visible:*— keyboard focus:active/active:*— pressed:disabled/disabled:*— set via thedisabledprop
Anatomy
The named regions a consumer composes when rendering this component. Each is documented separately so you can target keyboard nav, ARIA labels, and slot props with precision.
value / defaultValue / onChangeControlled vs uncontrolled value. For `type="range"` these accept `[low, high]` tuples; for the single-thumb modes they accept a `number`. `onChange` fires on every drag / click / keyboard step.
min / max / stepNumeric bounds and step granularity for the continuous and (via Arrow keys) discrete modes.
tickCount / tickLabelsDiscrete + range modes render `tickCount` tick marks (default 9). When `tickLabels` is passed and the type is `discrete`, the labels render under the rail aligned to each tick.
startLabel / endLabel / showLabelsStatic min/max labels under the rail (`"Min"` / `"Max"` by default). Pass `showLabels={false}` to hide them, or any ReactNode to render rich labels (e.g. price ranges with currency glyphs).
formatTooltip / formatRangeTooltip / showTooltipTooltip bubble customisation. `formatTooltip(value)` for single-thumb modes; `formatRangeTooltip(low, high)` for the range mode. Pass `showTooltip={false}` to suppress the bubble entirely.
Keyboard
Each handle carries `role="slider"` with `aria-valuemin/valuemax/valuenow/valuetext`. Arrow keys (`←/→` and `↑/↓`) step by `step` (continuous) or the discrete tick width; `PageUp` / `PageDown` move by 10 × step; `Home` / `End` snap to `min` / `max`. Range constraints prevent the low handle moving past the high handle and vice-versa. Disabled sliders set `tabIndex={-1}` on the handles and forward `aria-disabled` so SR still reads the value.
Accessibility
Every Oshon component ships axe-clean. We test in CI on every PR and publish the audit log per component.
- WCAG level
- 2.2 AA
- Screen readers tested
- VoiceOver (macOS), NVDA (Windows)
- Last axe audit
- 2026-05-20
Do / Don't
✓ Do
<Slider type="continuous" min={0} max={100} defaultValue={50} /><Slider
type="discrete"
min={0}
max={4}
tickCount={5}
tickLabels={["Free", "Pro", "Team", "Business", "Enterprise"]}
defaultValue={2}
/><Slider
type="range"
min={0}
max={1000}
defaultValue={[100, 600]}
formatRangeTooltip={(lo, hi) => `$${lo} – $${hi}`}
/>const [vol, setVol] = useState(50);
<Slider type="continuous" value={vol} onChange={setVol} />✗ Don't
<Slider type="continuous" value={50} defaultValue={20} onChange={...} />`defaultValue` is only consulted on the first render of an uncontrolled slider. When `value` is set, the slider becomes controlled and `defaultValue` is silently ignored. Pick one mode — either `defaultValue` (uncontrolled, omit `value`) or `value` + `onChange` (controlled).
<Slider type="continuous" min={0} max={100} value={150} />The slider clamps the displayed thumb to the rail but `aria-valuenow` then disagrees with what `value` reads as in your state. Clamp on the consumer side before passing `value`, or expand `max` to fit your domain.
<Slider type="range" value={[80, 20]} onChange={...} />The component does not auto-swap an inverted tuple — the low thumb renders at `80%` and the high thumb at `20%`, which leaves the active rail with a negative width. Always order the tuple as `[lo, hi]` with `lo <= hi`.
Design rationale
A single component covers all three pixel patterns from the Figma DS3.1-HANDOFF page 2710:7530 because they share 95% of the same a11y contract and rail rendering — splitting them into three exports would force consumers to refactor when the requirement evolves from "single thumb" to "range". The discriminant lives on a `type` prop so TypeScript can narrow the `value` and `onChange` signatures (single-thumb modes use a `number`; range mode uses a `[lo, hi]` tuple) without forcing a runtime check. The 48-px touch target on `size="mobile"` exceeds the WCAG 2.5.5 minimum (44 × 44 CSS px) so the slider remains reliably tappable on touch surfaces even when the visible handle dot is small.