Liqui Design

Toggle

A two-state button, flat in both of them, filling with accent while it is on.

Loading…

Installation

npx shadcn@latest add https://liqui.design/r/toggle.json

Installing more than one? Register the namespace once in components.json and drop the URLs:

components.json
{
  "registries": {
    "@liqui-design": "https://liqui.design/r/{name}.json"
  }
}
npx shadcn@latest add @liqui-design/toggle

Usage

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.

On this page