@cf/ui

@cf/switch

Switch

An on/off toggle for a setting that takes effect straight away, in default and sm sizes.

Base UI's Switch with the stock colours replaced. Track and thumb are built from foreground and background only, because the --input colours shadcn ships left a light thumb on a light track in dark mode.

The track carries an after: pseudo-element that pads the hit area by 12px sideways and 8px vertically, so the 32px switch is easier to hit without changing its size. In the notes menus the Switch is only a display: the menu row is the control, and the Switch is hidden from assistive tech and pointer events.

import { Switch } from "@/components/ui/switch";
 
<Switch aria-label="Confidential" checked={on} onCheckedChange={setOn} />

Props

PropTypeDefault
sizesm | defaultdefault

Rules

The same rules ship in DESIGN.md and the MCP server, for the agents building with this.

  • must

    Give an interactive Switch an accessible name, through a FieldLabel or an aria-label.

    name-the-switch

  • must

    When a whole row is the control (for example a DropdownMenuCheckboxItem), render the Switch with aria-hidden, tabIndex={-1} and pointer-events-none, and hide the row's tick with [&_[data-slot=menu-item-indicator]]:hidden.

    Otherwise there are two focusable controls and two state indicators for one setting.

    display-only-in-rows

  • should

    Use size="sm" inside menus and other dense rows, and default in forms.

    small-in-dense-rows

  • should

    Don't recolour the off state with --input; if you tint the on state, change only data-checked:bg-*.

    Track and thumb use foreground and background so off and on stay distinguishable in both themes; the stock --input colours left a light thumb on a light track in dark mode.

    keep-foreground-colours

Install

bunx shadcn@latest add @cf/switch

Needs the @cf registry in your components.json once. The theme and any sibling components come along automatically.

Source

switch.tsxShow
"use client";
 
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
import { cn } from "cn";
 
// Track and thumb are built from foreground/background only, so off and on
// stay distinguishable in both themes. The stock colours leaned on --input,
// which left a light thumb on a light track in dark mode.
function Switch({
  className,
  size = "default",
  ...props
}: SwitchPrimitive.Root.Props & {
  size?: "sm" | "default";
}) {
  return (
    <SwitchPrimitive.Root
      className={cn(
        "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent outline-none transition-all after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=sm]:h-[14px] data-[size=default]:w-[32px] data-[size=sm]:w-[24px] data-disabled:cursor-not-allowed data-checked:bg-foreground data-unchecked:bg-foreground/25 data-disabled:opacity-50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
        className
      )}
      data-size={size}
      data-slot="switch"
      {...props}
    >
      <SwitchPrimitive.Thumb
        className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-unchecked:translate-x-0"
        data-slot="switch-thumb"
      />
    </SwitchPrimitive.Root>
  );
}
 
export { Switch };