Welcome to Oshon · v1.0  ·  Now in public beta for enterprise teams Read the launch notes
SelectionUpdatedFreeWCAG 2.2 AA

Slider

Continuous · Discrete · Range — 5 sizes · keyboard a11y · tooltip + tick labels.

Preview

Live preview
@oshon-ai/components
Continuous — single thumb, free step
QuietLoud
Value: 60%
Discrete — snaps to ticks · custom labels
Selected: Team
Range — two thumbs · currency formatter
$0$1,000
Selected: $100 – $600
Sizes — xs / s / m / l / mobile
xs
0100
s
0100
m
0100
l
0100
mobile
0100
Disabled
MinMax

Installation

Install the runtime packages:

pnpm
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives

Or scaffold the component source directly into your codebase (shadcn-style):

pnpm
pnpm dlx @oshon-ai/cli add slider

Wire the tokens into your Tailwind v4 stylesheet:

css
/* 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.

tsx
'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.

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

AttributeValuesDescription
data-oshon-sizexs · s · m · l · mobileVisual size axis. Mirrors the `size` prop.
data-oshon-tierprimary · secondary · tertiaryVisual emphasis tier (Button family). Mirrors the `tier` prop.
data-oshon-stateenabled · active · error · disabledComponent surface state. Set automatically based on props.
data-disabledtrue · (omitted)Set when `disabled` is true. Pair with `:disabled` CSS for native input components.
data-stateopen · closed · checked · unchecked · …Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.).
css
/* 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 the disabled prop

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 / onChange

Controlled 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 / step

Numeric bounds and step granularity for the continuous and (via Arrow keys) discrete modes.

tickCount / tickLabels

Discrete + 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 / showLabels

Static 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 / showTooltip

Tooltip 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

Continuous slider, uncontrolled
<Slider type="continuous" min={0} max={100} defaultValue={50} />
Discrete with custom tick labels
<Slider
  type="discrete"
  min={0}
  max={4}
  tickCount={5}
  tickLabels={["Free", "Pro", "Team", "Business", "Enterprise"]}
  defaultValue={2}
/>
Range with currency formatter
<Slider
  type="range"
  min={0}
  max={1000}
  defaultValue={[100, 600]}
  formatRangeTooltip={(lo, hi) => `$${lo} – $${hi}`}
/>
Controlled with onChange
const [vol, setVol] = useState(50);
<Slider type="continuous" value={vol} onChange={setVol} />

✗ Don't

Mixing controlled value with uncontrolled defaultValue
<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).

Setting value past min/max bounds
<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.

Range with low > high
<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.