Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
labelstringThe setting's name, on SwitchField.
descriptionstringOne line under the name on what the setting does.
checkedbooleanControlled state.
defaultCheckedbooleanfalseStarting state when uncontrolled.
onCheckedChange(checked: boolean) => voidFires with the new state on every toggle.
disabledbooleanfalseFades the row and blocks toggling.
idstringId 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