Themes
Skins
Add a theme with the shadcn CLI, or copy its source. Each theme uses the click-wheel package.
0:003:12
Install with shadcn/ui
$
pnpm dlx shadcn@latest add https://click-wheel.jackymo.me/r/click-wheel-shadcn.json"use client";
import * as React from "react";
import { ClickWheel } from "click-wheel";
const GROOVE = 4;
const MAX_GROOVES = 8;
export interface WheelProps extends Omit<ClickWheel.RootProps, "children" | "className"> {
label: string;
getAriaValueText?: (value: number) => string;
icon?: React.ReactNode;
onCenterClick?: () => void;
centerLabel?: string;
className?: string;
grooves?: boolean;
}
export function Wheel({
label,
getAriaValueText,
icon,
onCenterClick,
centerLabel,
className,
grooves: wantGrooves = false,
min = 0,
max = 100,
unitsPerTurn = 100,
...rootProps
}: WheelProps) {
const laps = Math.ceil((max - min) / unitsPerTurn - 1e-9);
const grooves = wantGrooves && laps > 1 && laps <= MAX_GROOVES ? laps : 1;
return (
<ClickWheel.Root
{...rootProps}
min={min}
max={max}
unitsPerTurn={unitsPerTurn}
className={`relative aspect-square ${className ?? ""}`}
>
{Array.from({ length: grooves }, (_, i) => (
<div
key={i}
aria-hidden="true"
className="absolute rounded-full [background:conic-gradient(var(--primary)_calc(var(--groove-fill)*360deg),var(--border)_0)] [mask:radial-gradient(circle_closest-side,transparent_calc(100%_-_3px),black_calc(100%_-_2px))]"
style={
{
inset: i * GROOVE,
"--groove-fill":
grooves === 1
? "var(--click-wheel-fraction)"
: `clamp(0, calc(var(--click-wheel-turns) - ${i}), 1)`,
} as React.CSSProperties
}
/>
))}
<ClickWheel.Ring
aria-label={label}
getAriaValueText={getAriaValueText}
style={{ inset: grooves * GROOVE + 4 }}
className="absolute cursor-grab rounded-full border border-border bg-muted shadow-sm outline-none ring-offset-2 ring-offset-background focus-visible:ring-2 focus-visible:ring-ring data-[dragging]:cursor-grabbing data-[disabled]:cursor-default data-[disabled]:opacity-50"
>
<ClickWheel.Rotor className="absolute inset-0 rounded-full opacity-25 transition-opacity [background:repeating-conic-gradient(var(--ticks,var(--foreground))_0_1deg,transparent_1deg_15deg)] [mask:radial-gradient(circle_closest-side,transparent_56%,black_57%_82%,transparent_83%)] data-[dragging]:opacity-60 data-[coasting]:opacity-60" />
</ClickWheel.Ring>
<ClickWheel.Center
aria-label={centerLabel}
onClick={onCenterClick}
className="absolute left-1/2 top-1/2 flex size-[38%] -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-border [background:var(--hub,var(--background))] [color:var(--hub-foreground,var(--foreground))] shadow-xs outline-none transition-transform active:scale-95 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 [&>svg]:size-[36%]"
>
{icon}
</ClickWheel.Center>
</ClickWheel.Root>
);
}.theme-navy-orange {
--duo-primary: #12354e;
--duo-secondary: #f99d1b;
}
.theme-indigo-coral {
--duo-primary: #051230;
--duo-secondary: #f48067;
}
.theme-russet-sea {
--duo-primary: #793327;
--duo-secondary: #00b49b;
}
.theme-plum-cinnamon {
--duo-primary: #4e1d4c;
--duo-secondary: #c27544;
}
.theme-navy-orange,
.theme-indigo-coral,
.theme-russet-sea,
.theme-plum-cinnamon,
.dark .theme-navy-orange,
.dark .theme-indigo-coral,
.dark .theme-russet-sea,
.dark .theme-plum-cinnamon {
--primary: var(--duo-secondary);
--primary-foreground: #fff;
--muted: var(--duo-primary);
--border: color-mix(in oklab, var(--duo-primary) 62%, var(--duo-secondary));
--ring: var(--duo-secondary);
--hub: var(--duo-secondary);
--hub-foreground: #fff;
--ticks: var(--duo-secondary);
}
<div className="theme-navy-orange">
<Wheel … />
</div>