Toggle Row New
A settings row with a soft background and a switch. It can carry a description, and a row can be disabled when it depends on another one.
Installation
01
Install with the shadcn CLI. The NiceUI theme comes along the first time.
$ npx shadcn@latest add https://niceui.dev/r/design-toggle-row.json02
Or copy the source below (2 files). Dependencies: none.
Source
components/niceui/design/ToggleRow.tsx
import { useId, useState } from 'react';
import { cn } from '@/lib/cn';
import { LightFrame, Panel, SectionHead, focusRing } from './shared';
export type ToggleRowProps = {
label: string;
description?: string;
checked: boolean;
onChange: (v: boolean) => void;
disabled?: boolean;
};
export function SoftSwitch({ checked, onChange, label, disabled }: { checked: boolean; onChange: (v: boolean) => void; label: string; disabled?: boolean }) {
return (
<button
role="switch"
aria-checked={checked}
aria-label={label}
disabled={disabled}
onClick={() => onChange(!checked)}
className={cn('relative h-5 w-9 shrink-0 rounded-full transition-colors duration-200 disabled:opacity-40', focusRing, checked ? 'bg-zinc-900' : 'bg-zinc-200 ring-1 ring-inset ring-zinc-300/60')}
>
<span className={cn('absolute top-0.5 h-4 w-4 rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.25)] transition-all duration-300 ease-spring', checked ? 'left-[18px]' : 'left-0.5')} />
</button>
);
}
/** A full-width settings row: soft background, label on the left, switch on the right. */
export function ToggleRow({ label, description, checked, onChange, disabled }: ToggleRowProps) {
const id = useId();
return (
<div className={cn('rounded-xl bg-zinc-100 px-3 py-2.5 ring-1 ring-inset ring-zinc-200/70', disabled && 'opacity-60')}>
<div className="flex items-center justify-between gap-3">
<span id={id} className="text-[13px] text-zinc-700">{label}</span>
<SoftSwitch checked={checked} onChange={onChange} label={label} disabled={disabled} />
</div>
{description && <p className="mt-1 text-[12px] leading-snug text-zinc-500">{description}</p>}
</div>
);
}
export function ToggleRowDemo() {
const [holes, setHoles] = useState(false);
const [snap, setSnap] = useState(true);
const [outline, setOutline] = useState(true);
const [grid, setGrid] = useState(false);
return (
<LightFrame className="mx-auto max-w-[420px]">
<Panel className="space-y-5">
<div>
<SectionHead title="Outline" meta="3 paths · dark pixels" />
<div className="space-y-2">
<ToggleRow label="Fill holes" checked={holes} onChange={setHoles} />
<ToggleRow label="Keep stroke" description="Trace the outline as a stroke as well as a fill." checked={outline} onChange={setOutline} />
</div>
</div>
<div className="h-px bg-zinc-100" />
<div>
<SectionHead title="Canvas" meta={snap ? 'Snapping on' : 'Snapping off'} />
<div className="space-y-2">
<ToggleRow label="Snap to pixel grid" checked={snap} onChange={setSnap} />
<ToggleRow label="Show grid" checked={grid} onChange={setGrid} disabled={!snap} />
</div>
</div>
</Panel>
</LightFrame>
);
}components/niceui/design/shared.tsx
import type { ReactNode } from 'react';
import { cn } from '@/lib/cn';
/** A light surface for demos, since the doc stage itself is dark. */
export function LightFrame({ children, className, bare }: { children: ReactNode; className?: string; bare?: boolean }) {
return (
<div className={cn('w-full rounded-2xl font-sans', bare ? 'overflow-hidden bg-[#f4f4f5]' : 'bg-[#fafafa] p-4 sm:p-6', ' text-[13px] text-zinc-800 shadow-[0_1px_0_rgba(255,255,255,0.8)_inset,0_30px_60px_-30px_rgba(0,0,0,0.5)] ring-1 ring-zinc-200', className)}>
{children}
</div>
);
}
export function Panel({ children, className }: { children: ReactNode; className?: string }) {
return <div className={cn('rounded-2xl border border-zinc-200/80 bg-white p-4 shadow-[0_1px_2px_rgba(0,0,0,0.04)]', className)}>{children}</div>;
}
export function SectionHead({ title, meta }: { title: string; meta?: string }) {
return (
<div className="mb-3 flex items-baseline justify-between gap-3">
<h4 className="text-[13px] font-semibold text-zinc-900">{title}</h4>
{meta && <span className="truncate text-[12px] text-zinc-500">{meta}</span>}
</div>
);
}
export const focusRing = 'outline-none focus-visible:ring-2 focus-visible:ring-zinc-900/20 focus-visible:ring-offset-1';