Switch field
A settings row with a label, a description and a switch. Rows stack in a group.
npx shadcn@latest add https://ui.goodtabs.co/r/switch-field.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/switch-field
One row. Clicking the label or the description toggles the switch.
Group of three
Rows inside SwitchFieldGroup share hairlines and one border.
Disabled
A disabled row fades its text and switch and cannot be toggled.
Usage
import { SwitchField, SwitchFieldGroup } from "@/components/ui/switch-field"
<SwitchFieldGroup aria-label="Notifications">
<SwitchField label="Email digest" description="One email a week." defaultChecked />
<SwitchField label="Product updates" onCheckedChange={setUpdates} />
</SwitchFieldGroup>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | The setting's name, on SwitchField. | |
| description | string | One line under the name on what the setting does. | |
| checked | boolean | Controlled state. | |
| defaultChecked | boolean | false | Starting state when uncontrolled. |
| onCheckedChange | (checked: boolean) => void | Fires with the new state on every toggle. | |
| disabled | boolean | false | Fades the row and blocks toggling. |
| id | string | Id for the switch. Generated when left out. |
Source
switch-field/switch-field.tsx
import * as React from "react";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
type SwitchFieldProps = Omit<React.ComponentProps<typeof Switch>, "id"> & {
/** The setting's name. */
label: string;
/** One line under the name on what the setting does. */
description?: string;
/** Id for the switch. Generated when left out. */
id?: string;
};
/**
* A settings row: label and description on the left, a switch on the right.
* The whole row is the label, so clicking anywhere toggles the switch.
*/
export function SwitchField({ label, description, id, className, ...props }: SwitchFieldProps) {
const generated = React.useId();
const switchId = id ?? generated;
const labelId = `${switchId}-label`;
const descriptionId = `${switchId}-description`;
return (
<Label
htmlFor={switchId}
data-slot="switch-field"
className={cn(
"group/switch-field flex cursor-pointer items-start justify-between gap-4 py-3 leading-normal font-normal in-data-[slot=switch-field-group]:px-4 has-data-disabled:cursor-not-allowed",
className,
)}
>
<span className="flex min-w-0 flex-col gap-0.5 group-has-data-disabled/switch-field:opacity-50">
<span id={labelId} className="text-sm font-medium text-foreground">
{label}
</span>
{description && (
<span id={descriptionId} className="text-sm text-muted-foreground">
{description}
</span>
)}
</span>
<Switch
id={switchId}
aria-labelledby={labelId}
aria-describedby={description ? descriptionId : undefined}
className="mt-0.5"
{...props}
/>
</Label>
);
}
/** Stacks SwitchFields with hairlines between them inside one bordered card. */
export function SwitchFieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="group"
data-slot="switch-field-group"
className={cn("flex w-full flex-col divide-y rounded-lg border bg-card", className)}
{...props}
/>
);
}
For AI agents
Hand your agent this page, the markdown at /components/switch-field/prompt.md, or the Copy AI prompt button above.
When to use
- Settings and preferences pages with on and off options
- Notification, privacy and feature toggles that need a line of explanation
- Any list of independent booleans that should read as one card
When not to use
- Choosing one of several options: use a radio group or segmented tabs
- Actions that run straight away, like Send or Delete: use a button
- A switch inside a dense toolbar with no room for a description: use the Switch primitive alone
Agent notes
- Server component by default; it forwards every shadcn Switch prop. Pass onCheckedChange from a client component
- The row is the Label, so the hit area is the whole row; the switch is named by the label text only, not the description
- A row on its own has no border or side padding; SwitchFieldGroup adds the border, the hairlines and px-4
- Give SwitchFieldGroup an aria-label so screen readers announce the group