Toggle
A two-state button, flat in both of them, filling with accent while it is on.
Installation
npx shadcn@latest add https://liqui.design/r/toggle.jsonInstalling more than one? Register the namespace once in components.json and
drop the URLs:
{
"registries": {
"@liqui-design": "https://liqui.design/r/{name}.json"
}
}npx shadcn@latest add @liqui-design/toggleUsage
import { Toggle } from '@/components/ui/toggle';<Toggle defaultPressed>Focus</Toggle>const [focus, setFocus] = React.useState(true);
<Toggle pressed={focus} onPressedChange={setFocus}>
Focus
</Toggle>;Put several in a Toggle Group to share one glass strip and one arrow-key roving focus.
Notes
On is a fill
A toggle is a checkbox that happens to be shaped like a button, so it is flat
like one: --lq-control with a hairline at rest, --lq-accent when it is on.
'bg-[var(--lq-control)] shadow-[inset_0_0_0_1px_var(--lq-control-rim)]',
'data-[pressed]:bg-[var(--lq-accent)] data-[pressed]:text-white',This is the one place the component parts company with Button, which it otherwise matches in size, shape and press animation. A button's default variant is glass because pressing it does something; a toggle's whole job is to hold a state you can read back off it, and a value gets a solid fill.
Two surfaces, chosen by context
Standalone, a toggle draws that flat control surface itself, so it reads as a control against a wallpaper. Inside a Toggle Group it draws no fill at all: the strip is the surface there, the strip is still glass, and a toggle on it is a wash.
data-[pressed] means --lq-accent in both, so a toggle means the same thing
wherever it is standing. What changes is the resting state and the focus ring —
inside the strip the ring is inset, because an offset outline would sit on top
of the neighbouring toggle.
Which surface it is on is read from context, not passed as a prop; see Flattening is context, not a prop.
Don't render it as a Button
It looks like the obvious composition and it is the one thing to avoid:
// Don't.
<Toggle render={<Button />}>Focus</Toggle>Both components use data-pressed, and they mean different things by it. Base
UI puts data-pressed on a pressable trigger while its popup is open — a
momentary state, which is why Button shrinks to 0.97
on it. On a Toggle the same attribute is the latched on-state, so a button
composed this way stays permanently scaled down while it is on.
Toggle is therefore its own component rather than a wrapper around Button,
and matches it rather than composing with it. It presses the same way —
active:scale-[0.97], which only lasts as long as the pointer is down.
It is a native <button>
Both surfaces are, in both states. Toggle has no glass anatomy to put
anywhere, so none of the reasons Button's glass variant opts out of the native
element
apply here, and a composing trigger never needs nativeButton={false} with a
Toggle inside it.