Toggle row
A settings row pairing a label and hint with a switch, divided from its neighbours by a hairline.
The label is a real label for the switch, so clicking the text toggles it. Consecutive rows draw one hairline between them and a lone row draws none, so a list of them needs no dividers of its own.
Install
$
npx shadcn@latest add @vibra/toggle-rowNeeds the @vibra registry in your components.json — set it up once.
Examples
"use client"
import * as React from "react"
import { BellIcon, MailIcon, ShieldCheckIcon } from "lucide-react"
import { ToggleRow } from "@/components/ui/toggle-row"
export default function ToggleRowDemo() {
const [settings, setSettings] = React.useState({
digest: true,
alerts: true,
audit: false,
})
const set = (key: keyof typeof settings) => (checked: boolean) =>
setSettings((previous) => ({ ...previous, [key]: checked }))
return (
<div className="w-full max-w-md rounded-lg border px-4 py-1">
<ToggleRow
icon={<MailIcon />}
label="Weekly digest"
description="A Monday summary of every workspace you own"
checked={settings.digest}
onCheckedChange={set("digest")}
/>
<ToggleRow
icon={<BellIcon />}
label="Usage alerts"
description="Email me when a workspace passes 80% of its plan"
checked={settings.alerts}
onCheckedChange={set("alerts")}
/>
<ToggleRow
icon={<ShieldCheckIcon />}
label="Audit log streaming"
description="Available on the Scale plan"
checked={settings.audit}
onCheckedChange={set("audit")}
disabled
/>
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | React.ReactNode | — | What the switch controls; also names it. |
| description | React.ReactNode | — | One line under the label saying what turning it on does. |
| checked | boolean | — | Whether the switch is on. |
| onCheckedChange | (checked: boolean) => void | — | Called with the new state from the switch or from the label. |
| disabled | boolean | false | Dims the row and stops the switch. |
| icon | React.ReactNode | — | Sits before the label; sized to 4 unless it sets its own size. |
| id | string | generated | Binds the label to the switch; one is generated when it is left out. |
Dependencies
Registry
Source
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
export type ToggleRowProps = Omit<React.ComponentProps<"div">, "onChange"> & {
label: React.ReactNode
description?: React.ReactNode
checked: boolean
onCheckedChange: (checked: boolean) => void
disabled?: boolean
/** Sits before the label; sized to 4 unless it sets its own size. */
icon?: React.ReactNode
/** Binds the label to the switch; one is generated when it is left out. */
id?: string
}
/** A settings row that pairs a label and hint with a switch, so clicking the text toggles it. */
function ToggleRow({
className,
label,
description,
checked,
onCheckedChange,
disabled = false,
icon,
id,
...props
}: ToggleRowProps) {
const generatedId = React.useId()
const switchId = id ?? generatedId
const descriptionId = `${switchId}-description`
return (
<div
data-slot="toggle-row"
data-checked={checked || undefined}
data-disabled={disabled || undefined}
// [&+&] is ".this-class + .this-class": consecutive rows draw one hairline
// between them, and a lone row draws none.
className={cn(
"flex w-full items-start justify-between gap-4 py-3 [&+&]:border-t",
disabled && "opacity-50",
className
)}
{...props}
>
<div className="flex min-w-0 items-start gap-3">
{icon ? (
<span
data-slot="toggle-row-icon"
aria-hidden="true"
className="mt-0.5 text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
>
{icon}
</span>
) : null}
<div className="flex min-w-0 flex-col gap-1">
{/* A real <label for>: <button role="switch"> is a labelable element,
so clicking the text activates the switch itself. */}
<Label
htmlFor={switchId}
className={cn("w-fit", !disabled && "cursor-pointer")}
>
{label}
</Label>
{description ? (
<p
id={descriptionId}
data-slot="toggle-row-description"
className="text-xs text-muted-foreground"
>
{description}
</p>
) : null}
</div>
</div>
<Switch
id={switchId}
// The hint says what turning this on actually does, so it belongs to the
// switch and not only to the sighted reader.
aria-describedby={description ? descriptionId : undefined}
checked={checked}
disabled={disabled}
// Base UI hands the handler an event-details second argument; the
// public signature is just the new state.
onCheckedChange={(next) => onCheckedChange(next)}
className="mt-0.5 shrink-0"
/>
</div>
)
}
export { ToggleRow }