- Accordion
- Alert
- Alert Dialog
- Avatar
- Badge
- Button
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Circular Progress
- Collapsible
- Compare Slider
- Dialog
- Dropdown Menu
- Gauge
- Input
- Label
- Marquee
- Navigation Menu
- Number Field
- Pagination
- Popover
- Progress
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Tooltip
Demos
Drag the handle, or focus the slider and use the arrow keys, to reveal more of either side.
"use client"
import {
CompareSlider,
CompareSliderAfter,
CompareSliderBefore,
CompareSliderHandle,
} from "@workspace/ui/components/compare-slider"
function Panel({
title,
value,
className,
}: {
title: string
value: string
className?: string
}) {
return (
<div
className={`flex size-full flex-col items-center justify-center gap-2 ${className ?? ""}`}
>
<span className="text-sm opacity-80">{title}</span>
<span className="text-5xl font-semibold tabular-nums">{value}</span>
<span className="text-sm opacity-80">Vitamin D, ng/mL</span>
</div>
)
}
export function CompareSliderDemo() {
return (
<CompareSlider
defaultValue={50}
className="h-64 w-full max-w-md rounded-lg border"
aria-label="Compare results before and after supplements"
>
<CompareSliderBefore label="Before">
<Panel title="January" value="18" className="bg-muted" />
</CompareSliderBefore>
<CompareSliderAfter label="After">
<Panel
title="June"
value="42"
className="bg-primary text-primary-foreground"
/>
</CompareSliderAfter>
<CompareSliderHandle />
</CompareSlider>
)
}
Usage
Use a compare slider when the difference between two states is the point: results before and after a change, a light and dark theme, an old and new design. Both views share the same frame, so they should be the same size and line up. For more than two views, use tabs or a carousel.
Import it from @workspace/ui/components/compare-slider.
Anatomy
- CompareSlider — The root. Holds the position and has the
sliderrole. - CompareSliderBefore — The first view, shown on the left (or top when vertical).
- CompareSliderAfter — The second view, shown on the right (or bottom when vertical).
- CompareSliderHandle — The divider and grip. Pass children to replace the default grip.
- CompareSliderLabel — A badge pinned to a corner. CompareSliderBefore and CompareSliderAfter render one for you when you pass
label.
Notes
- Give the root a height, or a parent with a height for the vertical orientation. The views fill it absolutely.
- Give the root an
aria-labelso screen readers know what is being compared. Passinglabelto a view also names it for assistive technology. - The arrow keys move the handle by
step. Page Up, Page Down and Shift with an arrow move it by ten steps, and Home and End jump to either edge. valueis a percentage from0to100:0shows all of the after view,100shows all of the before view.
Examples
Vertical
Set orientation="vertical" to stack the views and drag up and down.
"use client"
import {
CompareSlider,
CompareSliderAfter,
CompareSliderBefore,
CompareSliderHandle,
} from "@workspace/ui/components/compare-slider"
export function CompareSliderVertical() {
return (
<div className="h-72 w-full max-w-xs">
<CompareSlider
orientation="vertical"
className="w-full rounded-lg border"
aria-label="Compare sleep before and after"
>
<CompareSliderBefore label="Before">
<div className="flex size-full flex-col items-center justify-center gap-1 bg-muted">
<span className="text-4xl font-semibold tabular-nums">5h 40m</span>
<span className="text-sm text-muted-foreground">Average sleep</span>
</div>
</CompareSliderBefore>
<CompareSliderAfter label="After">
<div className="flex size-full flex-col items-center justify-center gap-1 bg-secondary text-secondary-foreground">
<span className="text-4xl font-semibold tabular-nums">7h 25m</span>
<span className="text-sm opacity-80">Average sleep</span>
</div>
</CompareSliderAfter>
<CompareSliderHandle />
</CompareSlider>
</div>
)
}
Follow pointer
Set interaction="hover" to move the divider with the pointer, without dragging. The grip is hidden in this mode.
"use client"
import {
CompareSlider,
CompareSliderAfter,
CompareSliderBefore,
CompareSliderHandle,
} from "@workspace/ui/components/compare-slider"
export function CompareSliderHover() {
return (
<CompareSlider
interaction="hover"
className="h-56 w-full max-w-md rounded-lg border"
aria-label="Compare light and dark theme"
>
<CompareSliderBefore label="Light">
<div className="flex size-full items-center justify-center bg-white text-2xl font-semibold text-black">
Predict
</div>
</CompareSliderBefore>
<CompareSliderAfter label="Dark">
<div className="flex size-full items-center justify-center bg-black text-2xl font-semibold text-white">
Predict
</div>
</CompareSliderAfter>
<CompareSliderHandle />
</CompareSlider>
)
}
Controlled
Pass value and onValueChange to drive the position from your own state.
"use client"
import * as React from "react"
import { Button } from "@workspace/ui/components/button"
import {
CompareSlider,
CompareSliderAfter,
CompareSliderBefore,
CompareSliderHandle,
} from "@workspace/ui/components/compare-slider"
export function CompareSliderControlled() {
const [value, setValue] = React.useState(50)
return (
<div className="flex w-full max-w-md flex-col gap-4">
<CompareSlider
value={value}
onValueChange={setValue}
className="h-56 w-full rounded-lg border"
aria-label="Compare HbA1c before and after"
>
<CompareSliderBefore label="Before">
<div className="flex size-full flex-col items-center justify-center gap-1 bg-muted">
<span className="text-4xl font-semibold tabular-nums">6.1%</span>
<span className="text-sm text-muted-foreground">HbA1c</span>
</div>
</CompareSliderBefore>
<CompareSliderAfter label="After">
<div className="flex size-full flex-col items-center justify-center gap-1 bg-primary text-primary-foreground">
<span className="text-4xl font-semibold tabular-nums">5.4%</span>
<span className="text-sm opacity-80">HbA1c</span>
</div>
</CompareSliderAfter>
<CompareSliderHandle />
</CompareSlider>
<div className="flex items-center justify-between gap-2">
<span className="text-sm text-muted-foreground tabular-nums">
{Math.round(value)}%
</span>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => setValue(0)}>
After
</Button>
<Button variant="outline" size="sm" onClick={() => setValue(50)}>
Reset
</Button>
<Button variant="outline" size="sm" onClick={() => setValue(100)}>
Before
</Button>
</div>
</div>
</div>
)
}
Props
These are the props you set most often. Unlisted props pass through to the underlying element.
CompareSlider
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Controlled position, from 0 to 100. |
defaultValue | number | 50 | Starting position when uncontrolled. |
onValueChange | (value: number) => void | — | Called when the position changes. |
step | number | 1 | How far the arrow keys move the handle. |
interaction | "drag" or "hover" | "drag" | Drag the handle, or follow the pointer. |
orientation | "horizontal" or "vertical" | "horizontal" | Direction the divider moves. |
render | ReactElement or function | — | Replaces the root div with your own element. |
className | string | — | Classes merged onto the root. |
CompareSliderBefore and CompareSliderAfter
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Shows a corner label and names the view for screen readers. |
className | string | — | Classes merged onto the view. |
CompareSliderLabel
| Prop | Type | Default | Description |
|---|---|---|---|
side | "before" or "after" | — | Which corner to pin the label to. |