Component
Switch With Status
Switch paired with an explicit text status readout (Enabled / Disabled).
- Component
- React
- TSX
- Tailwind CSS
- MIT
Preview
Live component
9:41 100%
devsnips.dev/library/react/components/switches/switch-with-status/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Switches/switch-with-status React/Components/Switches/switch-with-status import type { ChangeEvent, ReactNode } from "react";
import { useId, useState } from "react";
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
export interface SwitchWithStatusProps {
label: ReactNode;
/** Status text shown when the switch is on. Defaults to "Enabled". */
onText?: ReactNode;
/** Status text shown when the switch is off. Defaults to "Disabled". */
offText?: ReactNode;
checked?: boolean;
defaultChecked?: boolean;
onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
name?: string;
value?: string | number | readonly string[];
id?: string;
className?: string;
}
/**
* A switch paired with an explicit status readout (e.g. "Enabled" /
* "Disabled"). The state is communicated by the tracked text — never color
* alone. The status line is wired to the input with `aria-describedby`.
* Built on the native `<input type="checkbox" role="switch">`.
*/
export function SwitchWithStatus({
label,
onText = "Enabled",
offText = "Disabled",
checked,
defaultChecked,
onChange,
disabled,
name,
value,
id,
className,
}: SwitchWithStatusProps) {
const generatedId = useId();
const inputId = id ?? `switch-${generatedId}`;
const statusId = `${inputId}-status`;
const isControlled = checked !== undefined;
const [internal, setInternal] = useState<boolean>(defaultChecked ?? false);
const isChecked = isControlled ? checked : internal;
function handleChange(event: ChangeEvent<HTMLInputElement>) {
const next = event.target.checked;
if (!isControlled) setInternal(next);
onChange?.(next, event);
}
return (
<label
htmlFor={inputId}
className={cx(
"flex w-full items-center gap-3 text-left",
disabled ? "cursor-not-allowed" : "cursor-pointer",
className,
)}
>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm font-medium leading-5 text-[var(--ds-color-foreground)]">{label}</span>
<span
id={statusId}
className={cx(
"text-xs leading-4",
isChecked
? "text-[var(--ds-color-foreground)]"
: "text-[var(--ds-color-muted-foreground)]",
)}
>
{isChecked ? onText : offText}
</span>
</span>
<span className={cx("relative inline-flex h-[14px] w-[24px] shrink-0 items-center", disabled && "opacity-50")}>
<input
id={inputId}
type="checkbox"
role="switch"
aria-checked={isChecked}
aria-describedby={statusId}
className={cx(
"absolute inset-0 h-full w-full cursor-pointer appearance-none rounded-full border transition-colors duration-150 ease-out hover:border-[var(--ds-color-border-strong)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:cursor-not-allowed motion-reduce:transition-none",
isChecked
? "border-[var(--ds-color-primary)] bg-[var(--ds-color-primary)]"
: "border-[var(--ds-color-border)] bg-[var(--ds-color-input)]",
)}
checked={isControlled ? isChecked : undefined}
defaultChecked={isControlled ? undefined : defaultChecked}
disabled={disabled}
name={name}
value={value}
onChange={handleChange}
/>
<span
aria-hidden="true"
className={cx(
"pointer-events-none absolute left-[2px] top-[2px] size-[10px] rounded-full transition-[transform,background-color] duration-150 ease-out motion-reduce:transition-none",
isChecked ? "translate-x-[10px]" : "translate-x-0",
isChecked
? "bg-[var(--ds-color-primary-foreground)]"
: "bg-[var(--ds-color-muted-foreground)]",
)}
/>
</span>
</label>
);
}
export default SwitchWithStatus; /* DevSnips React — JavaScript parity build.
* Same API, behavior, and classes as code.tsx; TypeScript types removed.
* Regenerated from code.tsx — edit code.tsx and re-run the generator.
*/
import { useId, useState } from "react";
function cx(...parts) {
return parts.filter(Boolean).join(" ");
}
export function SwitchWithStatus({
label,
onText = "Enabled",
offText = "Disabled",
checked,
defaultChecked,
onChange,
disabled,
name,
value,
id,
className
}) {
const generatedId = useId();
const inputId = id ?? `switch-${generatedId}`;
const statusId = `${inputId}-status`;
const isControlled = checked !== undefined;
const [internal, setInternal] = useState(defaultChecked ?? false);
const isChecked = isControlled ? checked : internal;
function handleChange(event) {
const next = event.target.checked;
if (!isControlled) setInternal(next);
onChange?.(next, event);
}
return <label
htmlFor={inputId}
className={cx(
"flex w-full items-center gap-3 text-left",
disabled ? "cursor-not-allowed" : "cursor-pointer",
className
)}
>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm font-medium leading-5 text-[var(--ds-color-foreground)]">{label}</span>
<span
id={statusId}
className={cx(
"text-xs leading-4",
isChecked ? "text-[var(--ds-color-foreground)]" : "text-[var(--ds-color-muted-foreground)]"
)}
>
{isChecked ? onText : offText}
</span>
</span>
<span className={cx("relative inline-flex h-[14px] w-[24px] shrink-0 items-center", disabled && "opacity-50")}>
<input
id={inputId}
type="checkbox"
role="switch"
aria-checked={isChecked}
aria-describedby={statusId}
className={cx(
"absolute inset-0 h-full w-full cursor-pointer appearance-none rounded-full border transition-colors duration-150 ease-out hover:border-[var(--ds-color-border-strong)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:cursor-not-allowed motion-reduce:transition-none",
isChecked ? "border-[var(--ds-color-primary)] bg-[var(--ds-color-primary)]" : "border-[var(--ds-color-border)] bg-[var(--ds-color-input)]"
)}
checked={isControlled ? isChecked : undefined}
defaultChecked={isControlled ? undefined : defaultChecked}
disabled={disabled}
name={name}
value={value}
onChange={handleChange}
/>
<span
aria-hidden="true"
className={cx(
"pointer-events-none absolute left-[2px] top-[2px] size-[10px] rounded-full transition-[transform,background-color] duration-150 ease-out motion-reduce:transition-none",
isChecked ? "translate-x-[10px]" : "translate-x-0",
isChecked ? "bg-[var(--ds-color-primary-foreground)]" : "bg-[var(--ds-color-muted-foreground)]"
)}
/>
</span>
</label>;
}
export default SwitchWithStatus; # Switch With Status
Switch paired with an explicit text status readout (Enabled / Disabled).
## Usage
```tsx
<SwitchWithStatus label="Analytics" defaultChecked />
```
## JavaScript
A `code.jsx` build is provided for projects that ship plain JSX. It exposes the same API and behavior as `code.tsx` — only the TypeScript types are removed.
```jsx
<SwitchWithStatus label="Analytics" defaultChecked />
```
## Props
| Name | Type | Default | Description |
|---|---|---:|---|
| `label` | `ReactNode` (required) | — | Visible label. |
| `onText` / `offText` | `ReactNode` | `Enabled` / `Disabled` | Status readout for each state. |
| `checked` / `defaultChecked` | `boolean` | `false` | Controlled / uncontrolled checked state. |
| `onChange` | `(checked, event) => void` | — | Change callback. |
| `disabled` | `boolean` | — | Disables the control. |
| `name` / `value` / `id` | `string` | — | Native form attrs. |
## States
Native switch with an explicit status readout beneath the label. The status text tracks the checked state (`Enabled` / `Disabled` by default, overridable with `onText` / `offText`), so the state is readable in words — never color alone. The readout is wired to the input with `aria-describedby`.
## Accessibility
`<label htmlFor>` association; the status line is linked with `aria-describedby` so assistive tech announces the current state as supporting text. Visible `focus-visible` ring.
## Styling
Built with React, Tailwind CSS, and DevSnips design tokens. The component consumes the `--ds-*` semantic tokens via arbitrary values (for example `bg-[var(--ds-color-primary)]`). Define the tokens once in your theme — no component-specific CSS file is required.
## Design Tokens
See [React/DESIGN_TOKENS.md](../../../DESIGN_TOKENS.md) for the authoritative token specification. This switch uses the semantic color, radius, spacing, and motion tokens.
## Notes
Use this when the current state must be unambiguous at a glance — integrations, automation, sync. For a plain setting the thumb + track are enough; reach for `switch-with-status` when the words themselves matter. 114 lines UTF-8 · LF · Spaces: 2
Continue browsing