Toggle
A pressable Base UI toggle styled with shadcn conventions. Use it for a single on/off control that lives in a toolbar, not in a form.
Installation
npx shadcn@latest add https://sketchcn.chuwii.com/r/toggle.jsonStyles
Toggle leans on these rules from sketch.css. The shadcn CLI installs the file with sketch-provider; copying the component by hand means copying them too.
[data-sketch-outline] path {
stroke-dasharray: var(--sketch-dash, none);
animation: var(--sketch-dash-animation, none);
}
@keyframes sketch-dash-boil {
to {
stroke-dashoffset: -10;
}
}
[data-sketch-outline] path[fill]:not([fill="none"]) {
fill: var(--sketch-fill);
fill-opacity: var(--sketch-fill-opacity, 1);
transition:
fill 150ms ease,
fill-opacity 150ms ease;
}Read the whole file for the drawing variables, paper textures and reduced-motion rules.
Usage
import { Bold } from "@matita/icons/react";
import { Toggle } from "@/components/ui/toggle";
export function BoldToggle() {
return (
<Toggle aria-label="Bold" defaultPressed>
<Bold />
Bold
</Toggle>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "outline" | "default" | Outline draws a sketch border even when unpressed. |
size | "sm" | "default" | "lg" | "default" | Height, padding and icon scale. |
Everything else, including the pressed state props, is forwarded to the Base UI Toggle primitive. The component also exports toggleVariants, TOGGLE_VARIANTS and TOGGLE_SIZES.
Examples
Formatting toolbar
import { Toggle } from "@/components/ui/toggle";
const TEXT_MARKS = [
{ value: "bold", label: "Bold", Icon: Bold },
{ value: "italic", label: "Italic", Icon: Italic },
{ value: "underline", label: "Underline", Icon: Underline },
];
export function TextToolbar() {
const [marks, setMarks] = useState<string[]>(["bold"]);
function toggleMark(value: string, pressed: boolean) {
setMarks((current) => {
if (pressed) {
return [...current, value];
}
return current.filter((mark) => mark !== value);
});
}
return (
<div className="flex gap-1">
{TEXT_MARKS.map(({ value, label, Icon }) => (
<Toggle
key={value}
variant="outline"
size="sm"
aria-label={label}
pressed={marks.includes(value)}
onPressedChange={(pressed) => toggleMark(value, pressed)}
>
<Icon />
</Toggle>
))}
</div>
);
}Controlled filter
import { Toggle } from "@/components/ui/toggle";
export function DraftsFilter() {
const [showDrafts, setShowDrafts] = useState(false);
return (
<Toggle pressed={showDrafts} onPressedChange={setShowDrafts}>
Show drafts
</Toggle>
);
}Animation
A pressed toggle dashes its outline and swaps dashes and gaps with the sketch-dash-boil keyframe from sketch.css, so the border reads as redrawn by hand.
"data-pressed:[--sketch-dash:5_5] data-pressed:[--sketch-dash-animation:sketch-dash-boil_1s_steps(2)_infinite]"Hovering raises the fill instead, through --sketch-fill-opacity.
"hover:[--sketch-fill-opacity:0.08] data-pressed:[--sketch-fill-opacity:0.14]"<Toggle className="data-pressed:[--sketch-dash-animation:none]">Quiet</Toggle>