Bevel

Guarded switch

A critical switch under a flip-up cover, as in a cockpit. Lift the cover, then flip it; closing the cover returns it to safe, and the cover closes itself if left.

Agent permissions and deploys

Lift a cover, then flip the switch. Leave it and the cover closes, turning it off.

Agent shell access
Off, guarded
Deploy to prod
Off, guarded

The agent asks before running shell commands.

Installation

Add with the CLI

  1. Once per project, add the Bevel registry to components.json.

    components.json
    {
      "registries": {
        "@bevel": "https://bevel-ui.vercel.app/r/{name}.json"
      }
    }
  2. Add Guarded switch. This brings in the Bevel foundation, installs @base-ui/react and cn, and merges the styles into your global CSS.

    Terminal
    npx shadcn@latest add @bevel/guarded-switch
Install manually
  1. Install the dependencies.

    Terminal
    pnpm add @base-ui/react cn
  2. Add the Bevel foundation, if you haven't already. Light, depth, materials, motion and shape tokens, plus the utilities components use to apply them.

    styles/bevel.css
    /*
     * Bevel foundation. Every component builds on these tokens and utilities;
     * component stylesheets only add their own palettes and shapes.
     *
     * Layers:
     *   1. Light     one source, above and slightly in front of the screen.
     *   2. Depth     named elevations built from the light: recessed, flush,
     *                raised, pressed, object.
     *   3. Material  a surface recipe (leather, plastic, metal) that reads
     *                --bevel-material and --bevel-material-edge.
     *   4. Motion    two curves, a spring, and durations named by purpose.
     *   5. Shape     radii, stitching, and the type scale for amounts.
     */
    
    :root {
      /* 1. Light */
      --bevel-highlight: oklch(1 0 0 / 0.22);
      --bevel-highlight-strong: oklch(1 0 0 / 0.35);
      --bevel-shade: oklch(0 0 0 / 0.32);
      --bevel-shade-strong: oklch(0 0 0 / 0.5);
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
      /* Stamped into a surface: shaded on the upper inner edge, lit below. */
      --bevel-deboss: 0 -1px 0 oklch(0 0 0 / 0.45), 0 1px 0 oklch(1 0 0 / 0.12);
    
      /* 3. Material defaults; components and variants override these. */
      --bevel-material: oklch(0.47 0.085 52);
      --bevel-material-edge: oklch(0.3 0.06 44);
      --bevel-material-recess: oklch(0.2 0.008 260); /* neutral dark; materials set their own */
      --bevel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .7 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
      --bevel-grain-size: 180px;
    
      /* 2. Depth */
      --bevel-depth-recessed:
        inset 0 1px 2px var(--bevel-shade-strong),
        0 1px 0 var(--bevel-highlight);
      --bevel-depth-flush:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade);
      --bevel-depth-raised:
        inset 0 1px 0 var(--bevel-highlight),
        0 1px 0 var(--bevel-material-edge),
        0 2px 0 var(--bevel-material-edge),
        0 3px 5px var(--bevel-shade);
      --bevel-depth-pressed:
        inset 0 1px 2px var(--bevel-shade),
        0 1px 0 var(--bevel-highlight);
      /* A whole object resting on the page. */
      --bevel-depth-object:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade),
        0 0 0 1px var(--bevel-material-edge),
        0 1px 1px var(--bevel-cast),
        0 6px 12px -2px var(--bevel-cast),
        0 18px 32px -8px var(--bevel-cast);
      /* The shadow under a loose object, for filter: drop-shadow(). */
      --bevel-depth-cast-sm: 0 4px 6px var(--bevel-cast);
      /* A thin object (a card) lying on another surface. */
      --bevel-depth-sheet:
        inset 0 1px 0 var(--bevel-highlight-strong),
        inset 0 0 0 1px oklch(0 0 0 / 0.12),
        0 -1px 2px oklch(0 0 0 / 0.18);
    
      /* 4. Motion */
      --bevel-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
      --bevel-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
      /* Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. */
      --bevel-ease-spring: linear(0, 0.06, 0.202, 0.38, 0.561, 0.723, 0.856, 0.955, 1.023, 1.062, 1.081, 1.083, 1.075, 1.062, 1.047, 1.032, 1.019, 1.009, 1.002, 0.997, 0.994, 0.993, 0.993, 0.994, 1);
      --bevel-duration-press: 100ms; /* a control giving under a finger */
      --bevel-duration-small: 200ms; /* hover lifts, tabs, small state changes */
      --bevel-duration-move: 300ms; /* objects moving: cards, drawers */
      --bevel-duration-ambient: 520ms; /* whole-scene changes: lighting */
      --bevel-stagger: 30ms;
      --bevel-lift: 3px; /* how far an object rises on hover */
      --bevel-perspective: 30rem; /* viewing distance for parts that turn in 3D (covers, flaps) */
      /* Unchosen items in a group sit back in shadow. */
      --bevel-dim: 0.82;
    
      /* 5. Shape */
      --bevel-radius-xs: 0.25rem;
      --bevel-radius-sm: 0.5rem;
      --bevel-radius-md: 0.625rem;
      --bevel-radius-lg: 1rem;
      --bevel-radius-xl: 1.5rem;
      --bevel-stitch-width: 1.5px;
      --bevel-stitch-inset: 0.375rem;
      /* Amounts: the figure, its secondary parts (currency, cents), labels, fine print. */
      --bevel-amount: 2.25rem;
      --bevel-amount-minor: 1.25rem;
      --bevel-label: 0.75rem;
      --bevel-fine: 0.6875rem;
      --bevel-tracking-label: 0.08em; /* stamped labels */
      --bevel-tracking-print: 0.06em; /* printed marks on cards */
    
      /* Paper and ink */
      --bevel-paper: oklch(0.975 0.008 85);
      --bevel-paper-foreground: oklch(0.28 0.02 260);
      --bevel-paper-muted: oklch(0.48 0.015 260);
      --bevel-paper-rule: oklch(0.78 0.04 240 / 0.45);
      --bevel-paper-margin: oklch(0.68 0.14 25 / 0.45);
      --bevel-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9 .35' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E");
      /* Ink soaks unevenly: mostly solid with pale specks. Used as a mask. */
      --bevel-ink-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='i'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 2.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23i)'/%3E%3C/svg%3E");
      --bevel-ink-red: oklch(0.52 0.19 27);
      --bevel-ink-blue: oklch(0.44 0.14 262);
      --bevel-ink-green: oklch(0.5 0.12 155);
      --bevel-ink-black: oklch(0.3 0.01 260);
      --bevel-ink-amber: oklch(0.55 0.13 65);
    
      /* Brass and ivory: instrument hardware and dial faces */
      --bevel-brass: oklch(0.76 0.11 82);
      --bevel-brass-edge: oklch(0.52 0.08 70);
      --bevel-ivory: oklch(0.95 0.025 90);
      --bevel-ivory-foreground: oklch(0.25 0.015 60);
      --bevel-needle: oklch(0.55 0.2 27);
    
      /* Desk stationery: legal pads, pens, pencils, exhibit stickers */
      --bevel-pad: oklch(0.95 0.075 98); /* yellow legal pad */
      --bevel-pad-foreground: oklch(0.27 0.03 255);
      --bevel-pad-rule: oklch(0.72 0.06 235 / 0.45);
      --bevel-pad-margin: oklch(0.62 0.16 25 / 0.55);
      --bevel-pen-red: oklch(0.52 0.2 27); /* correcting pen */
      --bevel-pen-blue: oklch(0.42 0.15 262); /* writing pen */
      --bevel-pencil: oklch(0.55 0.01 260); /* not written in ink yet */
      --bevel-sticker: oklch(0.9 0.12 95); /* exhibit sticker */
      --bevel-sticker-foreground: oklch(0.26 0.03 70);
      /* Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. */
      --bevel-font-marker: "Permanent Marker", "Marker Felt", "Chalkboard SE", ui-rounded, "Arial Rounded MT Bold", system-ui, sans-serif;
    }
    
    .dark {
      --bevel-highlight: oklch(1 0 0 / 0.14);
      /* Paper under dim light, not inverted: it's still paper. */
      --bevel-paper: oklch(0.88 0.008 85);
      --bevel-ivory: oklch(0.86 0.022 90);
      --bevel-pad: oklch(0.86 0.07 98);
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /* Depth */
    @utility bevel-recessed {
      background-color: var(--bevel-material-recess);
      box-shadow: var(--bevel-depth-recessed);
    }
    @utility bevel-flush {
      box-shadow: var(--bevel-depth-flush);
    }
    @utility bevel-raised {
      background-color: color-mix(in oklch, var(--bevel-material), white 7%);
      background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 60%);
      box-shadow: var(--bevel-depth-raised);
    }
    @utility bevel-pressed {
      background-color: color-mix(in oklch, var(--bevel-material), black 4%);
      background-image: none;
      box-shadow: var(--bevel-depth-pressed);
    }
    @utility bevel-object {
      box-shadow: var(--bevel-depth-object);
    }
    
    /* Materials */
    @utility bevel-leather {
      background-color: var(--bevel-material);
      background-image:
        var(--bevel-grain),
        linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade));
      background-size: var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%;
      background-blend-mode: soft-light, normal;
    }
    @utility bevel-plastic {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(115deg, var(--bevel-highlight-strong), transparent 40%),
        linear-gradient(to bottom, transparent 25%, oklch(0 0 0 / 0.45));
      box-shadow: var(--bevel-depth-sheet);
    }
    @utility bevel-metal {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade) 100%),
        repeating-linear-gradient(90deg, oklch(1 0 0 / 0.06) 0 1px, transparent 1px 3px);
      box-shadow:
        inset 0 0 0 1px oklch(0 0 0 / 0.25),
        inset 0 1px 0 var(--bevel-highlight-strong);
    }
    
    /* Details */
    @utility bevel-deboss {
      text-shadow: var(--bevel-deboss);
    }
    @utility bevel-stitch {
      border-color: var(--bevel-stitch, oklch(0.88 0.05 82 / 0.8));
      border-style: dashed;
      border-width: var(--bevel-stitch-width);
      filter: drop-shadow(0 1px 0 var(--bevel-shade));
    }
    /* A lit fill sitting in a recess (meters, progress). Colour from --bevel-fill. */
    @utility bevel-fill {
      background-color: var(--bevel-fill);
      background-image: linear-gradient(to bottom, oklch(1 0 0 / 0.55), transparent 55%, oklch(0 0 0 / 0.18));
      box-shadow: 0 0 6px -1px var(--bevel-fill);
    }
    /* A small indicator light. Colour from --bevel-led. */
    @utility bevel-led {
      background-color: var(--bevel-led);
      background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / 0.85), transparent 45%);
      box-shadow:
        0 0 6px var(--bevel-led),
        inset 0 -1px 1px oklch(0 0 0 / 0.35),
        0 1px 0 var(--bevel-highlight);
    }
    
    /* Paper: a sheet resting on the surface, with fibre in it. */
    @utility bevel-paper {
      background-color: var(--bevel-paper);
      background-image: var(--bevel-fibre);
      background-size: 240px 240px;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
    /* Ink on paper: multiplies into the surface and soaks unevenly. Colour from --bevel-ink. */
    @utility bevel-ink {
      color: var(--bevel-ink, var(--bevel-ink-red));
      border-color: currentColor;
      mix-blend-mode: multiply;
      mask-image: var(--bevel-ink-texture);
      mask-size: 160px 160px;
    }
    /* Glass over an instrument window: one reflection from the light above. Put on an overlay element. */
    @utility bevel-glass {
      pointer-events: none;
      background-image: linear-gradient(165deg, var(--bevel-highlight-strong) 0%, transparent 38%, transparent 70%, var(--bevel-shade) 100%);
    }
    
    /* Material families: one per screen. Set on the screen's container. */
    [data-bevel-family="leather"] {
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
    }
    [data-bevel-family="paper"] {
      --bevel-cast: oklch(0.3 0.01 250 / 0.16);
    }
    .dark :is([data-bevel-family="leather"], [data-bevel-family="paper"]) {
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /*
     * Ruled paper: rules every --bevel-rule-height and a margin line at
     * --bevel-rule-margin. Set --bevel-sheet for the paper colour. The ruling
     * scrolls with the content.
     */
    @utility bevel-ruled {
      background-color: var(--bevel-sheet, var(--bevel-paper));
      background-image:
        linear-gradient(
          to right,
          transparent calc(var(--bevel-rule-margin) - 1px),
          var(--bevel-sheet-margin, var(--bevel-paper-margin)) calc(var(--bevel-rule-margin) - 1px) calc(var(--bevel-rule-margin) + 1px),
          transparent calc(var(--bevel-rule-margin) + 1px)
        ),
        repeating-linear-gradient(
          to bottom,
          transparent 0 calc(var(--bevel-rule-height) - 1px),
          var(--bevel-sheet-rule, var(--bevel-paper-rule)) calc(var(--bevel-rule-height) - 1px) var(--bevel-rule-height)
        ),
        var(--bevel-fibre);
      background-size: 100% 100%, 100% 100%, 240px 240px;
      background-attachment: local;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
  3. Copy guarded-switch.tsx into components/ui.

    components/guarded-switch.tsx
    "use client"
    
    import * as React from "react"
    import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
    import { cn } from "cn"
    
    type GuardedSwitchContextValue = {
      open: boolean
      setOpen: (open: boolean) => void
      disabled: boolean
      checked: boolean
      setChecked: (checked: boolean) => void
      touch: () => void
      ids: { control: string; label: string }
    }
    
    const GuardedSwitchContext = React.createContext<GuardedSwitchContextValue | null>(null)
    
    function useGuardedSwitch() {
      const context = React.useContext(GuardedSwitchContext)
      if (!context) throw new Error("GuardedSwitch parts must be inside <GuardedSwitch>")
      return context
    }
    
    /** Browsers may report a custom-property duration as "8000ms" or "8s". */
    function toMilliseconds(value: string, fallback: number) {
      const n = parseFloat(value)
      if (Number.isNaN(n)) return fallback
      return /ms\s*$/.test(value) ? n : /s\s*$/.test(value) ? n * 1000 : n
    }
    
    type GuardedSwitchProps = Omit<React.ComponentProps<"div">, "onChange"> & {
      checked?: boolean
      defaultChecked?: boolean
      onCheckedChange?: (checked: boolean) => void
      /** Closing the cover turns the switch off. Leave on for anything dangerous to leave running. */
      safeOnClose?: boolean
      /** Close the cover after --guarded-switch-close-after of no interaction. */
      autoClose?: boolean
      /** Locks the cover shut. Say why with aria-describedby on the root. */
      disabled?: boolean
    }
    
    /**
     * A critical switch under a flip-up cover. Lift the cover, then flip the
     * switch: two deliberate steps. Closing the cover returns it to safe.
     */
    function GuardedSwitch({
      className,
      checked: checkedProp,
      defaultChecked = false,
      onCheckedChange,
      safeOnClose = true,
      autoClose = true,
      disabled = false,
      children,
      ...props
    }: GuardedSwitchProps) {
      const [uncontrolled, setUncontrolled] = React.useState(defaultChecked)
      const checked = checkedProp ?? uncontrolled
      const [open, setOpenState] = React.useState(false)
      const rootRef = React.useRef<HTMLDivElement>(null)
      const timer = React.useRef<number | undefined>(undefined)
      const control = React.useId()
      const label = React.useId()
    
      const setChecked = React.useCallback(
        (next: boolean) => {
          if (checkedProp === undefined) setUncontrolled(next)
          onCheckedChange?.(next)
        },
        [checkedProp, onCheckedChange]
      )
    
      const setOpen = React.useCallback(
        (next: boolean) => {
          setOpenState(next)
          if (!next && safeOnClose && checked) setChecked(false)
        },
        [safeOnClose, checked, setChecked]
      )
    
      const touch = React.useCallback(() => {
        window.clearTimeout(timer.current)
        if (!autoClose || !rootRef.current) return
        const ms = toMilliseconds(
          getComputedStyle(rootRef.current).getPropertyValue("--guarded-switch-close-after"),
          8000
        )
        timer.current = window.setTimeout(() => setOpen(false), ms)
      }, [autoClose, setOpen])
    
      React.useEffect(() => {
        if (open) touch()
        else window.clearTimeout(timer.current)
        return () => window.clearTimeout(timer.current)
      }, [open, touch])
    
      const context = React.useMemo(
        () => ({ open, setOpen, disabled, checked, setChecked, touch, ids: { control, label } }),
        [open, setOpen, disabled, checked, setChecked, touch, control, label]
      )
    
      return (
        <GuardedSwitchContext value={context}>
          <div
            ref={rootRef}
            role="group"
            aria-labelledby={label}
            data-slot="guarded-switch"
            data-open={open ? "" : undefined}
            data-checked={checked ? "" : undefined}
            data-disabled={disabled ? "" : undefined}
            className={cn(
              "bevel-metal bevel-object inline-flex w-44 flex-col items-center gap-3 rounded-(--bevel-radius-md) p-4 text-(--guarded-switch-lever)",
              className
            )}
            {...props}
          >
            {children}
          </div>
        </GuardedSwitchContext>
      )
    }
    
    function GuardedSwitchLabel({ className, ...props }: React.ComponentProps<"span">) {
      const { ids } = useGuardedSwitch()
      return (
        <span
          id={ids.label}
          data-slot="guarded-switch-label"
          className={cn(
            "bevel-deboss text-center text-(length:--bevel-label) font-semibold tracking-(--bevel-tracking-label) uppercase",
            className
          )}
          {...props}
        />
      )
    }
    
    /** The cover and the switch beneath it. */
    function GuardedSwitchControl({ className }: { className?: string }) {
      const { open, setOpen, disabled, checked, setChecked, touch, ids } = useGuardedSwitch()
      return (
        <div
          data-slot="guarded-switch-housing"
          className={cn("relative mt-5 grid h-28 w-20 place-items-center perspective-(--bevel-perspective)", className)}
        >
          {/* Cover first: it is the first step, in focus order too. */}
          <button
            type="button"
            data-slot="guarded-switch-cover"
            aria-expanded={open}
            aria-controls={ids.control}
            disabled={disabled && !open}
            onClick={() => setOpen(!open)}
            className={cn(
              "guarded-switch-cover absolute inset-x-0 top-0 z-10 h-24 origin-top cursor-pointer rounded-(--bevel-radius-sm) outline-none",
              "transition-[rotate] duration-(--bevel-duration-move) ease-(--bevel-ease-out) motion-reduce:transition-none",
              "focus-visible:ring-3 focus-visible:ring-(--guarded-switch-on)/60 disabled:cursor-not-allowed",
              open ? "[rotate:x_var(--guarded-switch-open-angle)]" : "[rotate:x_0deg]"
            )}
          >
            <span className="sr-only">{open ? "Close the cover" : "Lift the cover"}</span>
          </button>
          <SwitchPrimitive.Root
            id={ids.control}
            data-slot="guarded-switch-switch"
            checked={checked}
            disabled={!open || disabled}
            aria-labelledby={ids.label}
            onCheckedChange={(next) => {
              setChecked(next)
              touch()
            }}
            className="bevel-recessed relative flex h-20 w-10 items-center justify-center rounded-(--bevel-radius-sm) outline-none focus-visible:ring-3 focus-visible:ring-(--guarded-switch-on)/60 data-disabled:cursor-not-allowed"
          >
            <SwitchPrimitive.Thumb
              data-slot="guarded-switch-lever"
              className="bevel-raised block h-8 w-5 translate-y-(--guarded-switch-throw) rounded-full transition-[translate] duration-(--bevel-duration-small) ease-(--bevel-ease-spring) data-checked:-translate-y-(--guarded-switch-throw) motion-reduce:transition-none"
            />
          </SwitchPrimitive.Root>
        </div>
      )
    }
    
    /** Lamp and words: Off, Armed (cover up), On. */
    function GuardedSwitchStatus({ className, ...props }: React.ComponentProps<"span">) {
      const { open, checked, disabled } = useGuardedSwitch()
      return (
        <span
          role="status"
          data-slot="guarded-switch-status"
          className={cn(
            "bevel-deboss flex items-center gap-1.5 text-(length:--bevel-fine) font-semibold tracking-(--bevel-tracking-print) uppercase",
            className
          )}
          {...props}
        >
          <span aria-hidden className="bevel-led size-1.5 rounded-full" />
          {checked ? "On" : disabled ? "Locked" : open ? "Cover open" : "Off, guarded"}
        </span>
      )
    }
    
    export { GuardedSwitch, GuardedSwitchLabel, GuardedSwitchControl, GuardedSwitchStatus }
    export type { GuardedSwitchProps }
  4. Copy guarded-switch.css into your styles.

    styles/guarded-switch.css
    /*
     * Guarded switch: a critical switch under a flip-up cover. Lift the cover
     * to use the switch; closing it returns the switch to safe. The cover
     * closes itself after a while, as in the Rockwell Collins virtual guarded
     * switch (US11548656). Builds on bevel.css (metal, raised, motion).
     */
    
    :root {
      --guarded-switch-plate: oklch(0.3 0.008 260);
      --guarded-switch-plate-edge: oklch(0.2 0.008 260);
      --guarded-switch-cover: oklch(0.58 0.21 27 / 0.72); /* translucent red: you can see the switch under it */
      --guarded-switch-cover-edge: oklch(0.42 0.18 27);
      --guarded-switch-lever: oklch(0.9 0.006 260);
      --guarded-switch-lever-edge: oklch(0.62 0.01 260);
      --guarded-switch-on: oklch(0.72 0.19 27);
      --guarded-switch-off: oklch(0.45 0.01 260);
      --guarded-switch-open-angle: -112deg; /* how far the cover swings up */
      --guarded-switch-throw: 0.75rem; /* lever travel either side of centre */
      --guarded-switch-close-after: 8s; /* idle time before the cover closes itself */
    }
    
    [data-slot="guarded-switch"] {
      --bevel-material: var(--guarded-switch-plate);
      --bevel-material-edge: var(--guarded-switch-plate-edge);
      --bevel-material-recess: var(--guarded-switch-plate-edge);
      --bevel-led: var(--guarded-switch-off);
    }
    [data-slot="guarded-switch"][data-checked] {
      --bevel-led: var(--guarded-switch-on);
    }
    [data-slot="guarded-switch-lever"] {
      --bevel-material: var(--guarded-switch-lever);
      --bevel-material-edge: var(--guarded-switch-lever-edge);
    }
    
    /* A moulded red cover: lit along its top edge, darker toward the hinge-free end. */
    @utility guarded-switch-cover {
      background-color: var(--guarded-switch-cover);
      background-image: linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade));
      box-shadow:
        inset 0 0 0 1px var(--guarded-switch-cover-edge),
        0 4px 8px -2px var(--bevel-cast);
    }
  5. Import the stylesheets after Tailwind in your global CSS, foundation first.

    globals.css
    @import "./bevel.css";
    @import "./guarded-switch.css";

Anatomy

Each part is its own component. Leave out the ones you don't need.

Anatomy
<GuardedSwitch checked={on} onCheckedChange={setOn}>
  <GuardedSwitchLabel />
  <GuardedSwitchControl />
  <GuardedSwitchStatus />
</GuardedSwitch>

Parts

PartRendersWhat it does
GuardedSwitchdiv (group)checked, defaultChecked, onCheckedChange. safeOnClose (default true) turns it off when the cover closes; autoClose closes the cover after --guarded-switch-close-after. disabled locks the cover shut; say why with aria-describedby.
GuardedSwitchLabelspanNames the group and the switch.
GuardedSwitchControlbutton + SwitchThe cover (a button with aria-expanded) over a Base UI Switch that's disabled while the cover is closed.
GuardedSwitchStatusspan (status)Off, cover open, or on, in words.