component — color
Color Thumb
The draggable handle inside a color area or color wheel, and the component you pass as ColorArea's children to replace the default one. Built on react-aria-components and styled with the quebi design system: the picked color is its fill, focus grows it and adds the focus ring.
- form
- color
- picker
- thumb
- handle
- interactive
02 examples
The default handle, and one of your own
A ColorThumb only means anything inside a color picker, and the reason to reach for it directly is ColorArea's `children`: the area renders `{children ?? <ColorThumb />}`, so a thumb you pass replaces its default rather than joining it. On the left the area draws that default; on the right the same component is larger, square-cornered and ringed in the focus ink. Both grow and take the focus ring when you tab to them.
Disabled, without dimming twice
A disabled ColorArea mutes its whole subtree with `opacity-50`, and ColorThumb carries a `disabled:opacity-50` of its own for the surfaces that do not — compounded, a nested thumb lands at a quarter and reads as broken rather than disabled. So a thumb passed to an area takes `disabled:opacity-100`, which is exactly what the area's own default carries. Here the fill stays legible under the muted gradient.
source
color-thumb.tsxCopy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import {
ColorThumb as ColorThumbPrimitive,
type ColorThumbProps,
composeRenderProps,
} from "react-aria-components"
import { cn } from "@/lib/utils"
/**
* ColorThumb — quebi design system
*
* The draggable handle inside a ColorArea or a ColorWheel. Built on
* react-aria-components. Round by nature: a white border so the picked color
* (user data, left untouched) reads on any field, inside a muted-ink ring.
* Focus grows the thumb and swaps the ring for the focus ink.
*
* It is a slot, not only a fixture: `ColorArea` renders its `children` and falls
* back to this thumb, so styling the handle means passing one of these with a
* `className` rather than reaching past the area. A thumb nested in an area wants
* `disabled:opacity-100` — the area's own `disabled:opacity-50` already covers
* its subtree, and the two compound to a quarter.
*
* `ColorSlider` does *not* use this component: its track is 1.5rem tall, so its
* handle is the smaller `ColorSliderThumb` declared beside it.
*/
export function ColorThumb({ className, ...props }: ColorThumbProps) {
return (
<ColorThumbPrimitive
{...props}
className={composeRenderProps(className, (resolved) =>
cn(
"top-[50%] left-[50%] size-6 rounded-full border-2 border-white",
"ring-1 ring-quebi-fg-muted",
"transition-[width,height] duration-150",
"focus-visible:size-8 focus-visible:ring-2 focus-visible:ring-quebi-focus focus-visible:ring-offset-0",
"disabled:opacity-50 disabled:forced-colors:border-[GrayText] disabled:forced-colors:bg-[GrayText]",
"forced-colors:border-[ButtonBorder]",
resolved,
),
)}
/>
)
}