Click Wheel

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
themes/shadcn/wheel.tsx
"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>
  );
}
globals.css
.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>