Component
Checkbox Readonly
Read-only checkbox that stays focusable but cannot be toggled by the user.
- Component
- React
- TSX
- Tailwind CSS
- MIT
Preview
Live component
9:41 100%
devsnips.dev/library/react/components/checkboxes/checkbox-readonly/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Checkboxes/checkbox-readonly React/Components/Checkboxes/checkbox-readonly 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 CheckboxReadonlyProps {
label: ReactNode;
checked: boolean;
onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
name?: string;
value?: string | number | readonly string[];
id?: string;
helperText?: ReactNode;
className?: string;
}
/**
* Read-only checkbox. The control cannot be changed by the user but is NOT
* disabled — it remains focusable and perceivable as part of the document
* (e.g. a permission that is fixed). The native input uses `readOnly` plus a
* `preventDefault` on change (browsers do not natively honor `readOnly` on
* checkboxes), so clicks and Space do not toggle the value.
*/
export function CheckboxReadonly({
label,
checked,
onChange,
name,
value,
id,
helperText,
className,
}: CheckboxReadonlyProps) {
const generatedId = useId();
const inputId = id ?? `checkbox-${generatedId}`;
const helperId = `${inputId}-helper`;
function handleChange(event: ChangeEvent<HTMLInputElement>) {
// Browsers do not honor `readOnly` on checkboxes — force the rendered
// state back so it cannot be toggled. The onChange callback still fires so
// a parent can log the (blocked) attempt if it wants.
event.preventDefault();
onChange?.(checked, event);
}
return (
<div className={cx("flex flex-col gap-1", className)}>
<label
htmlFor={inputId}
className="inline-flex cursor-default items-center gap-2.5 text-sm leading-5 text-[var(--ds-color-foreground)]"
>
<span className="relative inline-flex size-[18px] shrink-0 items-center justify-center">
<input
id={inputId}
type="checkbox"
readOnly
aria-readonly="true"
aria-describedby={helperText ? helperId : undefined}
className="size-[18px] cursor-default appearance-none rounded-[var(--ds-radius-xs)] border border-[var(--ds-color-border)] bg-[var(--ds-color-surface-subtle)] read-only:text-[var(--ds-color-muted-foreground)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] checked:border-[var(--ds-color-muted-foreground)] checked:bg-[var(--ds-color-muted-foreground)] motion-reduce:transition-none"
checked={checked}
name={name}
value={value}
onChange={handleChange}
/>
<span
aria-hidden="true"
className={cx(
"pointer-events-none absolute inset-0 flex items-center justify-center text-[var(--ds-color-primary-foreground)] transition-opacity duration-150 motion-reduce:transition-none",
checked ? "opacity-100" : "opacity-0",
)}
>
<svg className="size-[12px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3.5} strokeLinecap="round" strokeLinejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>
</span>
</span>
<span className="select-none">{label}</span>
</label>
{helperText ? (
<p id={helperId} className="pl-[26px] text-xs leading-4 text-[var(--ds-color-muted-foreground)]">
{helperText}
</p>
) : null}
</div>
);
}
export default CheckboxReadonly; /* 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 } from "react";
function cx(...parts) {
return parts.filter(Boolean).join(" ");
}
export function CheckboxReadonly({
label,
checked,
onChange,
name,
value,
id,
helperText,
className
}) {
const generatedId = useId();
const inputId = id ?? `checkbox-${generatedId}`;
const helperId = `${inputId}-helper`;
function handleChange(event) {
event.preventDefault();
onChange?.(checked, event);
}
return <div className={cx("flex flex-col gap-1", className)}>
<label
htmlFor={inputId}
className="inline-flex cursor-default items-center gap-2.5 text-sm leading-5 text-[var(--ds-color-foreground)]"
>
<span className="relative inline-flex size-[18px] shrink-0 items-center justify-center">
<input
id={inputId}
type="checkbox"
readOnly
aria-readonly="true"
aria-describedby={helperText ? helperId : undefined}
className="size-[18px] cursor-default appearance-none rounded-[var(--ds-radius-xs)] border border-[var(--ds-color-border)] bg-[var(--ds-color-surface-subtle)] read-only:text-[var(--ds-color-muted-foreground)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] checked:border-[var(--ds-color-muted-foreground)] checked:bg-[var(--ds-color-muted-foreground)] motion-reduce:transition-none"
checked={checked}
name={name}
value={value}
onChange={handleChange}
/>
<span
aria-hidden="true"
className={cx(
"pointer-events-none absolute inset-0 flex items-center justify-center text-[var(--ds-color-primary-foreground)] transition-opacity duration-150 motion-reduce:transition-none",
checked ? "opacity-100" : "opacity-0"
)}
>
<svg className="size-[12px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3.5} strokeLinecap="round" strokeLinejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>
</span>
</span>
<span className="select-none">{label}</span>
</label>
{helperText ? <p id={helperId} className="pl-[26px] text-xs leading-4 text-[var(--ds-color-muted-foreground)]">
{helperText}
</p> : null}
</div>;
}
export default CheckboxReadonly; # Checkbox Readonly
Read-only checkbox that stays focusable but cannot be toggled by the user.
## Usage
```tsx
<CheckboxReadonly label="System-managed permission" checked />
```
## 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
<CheckboxReadonly label="System-managed permission" checked />
```
## Props
| Name | Type | Default | Description |
|---|---|---:|---|
| `label` | `ReactNode` (required) | — | Visible label. |
| `checked` | `boolean` (required) | — | Fixed checked state. |
| `onChange` | `(checked, event) => void` | — | Fires with the blocked attempt. |
| `helperText` | `ReactNode` | — | Helper text. |
| `name` / `value` / `id` | `string` | — | Native form attrs. |
## States
Read-only checkbox. The native input uses `readOnly` plus a `preventDefault` on change (browsers do not natively honor `readOnly` on checkboxes), so clicks and Space do not toggle the value. Unlike `disabled`, it remains focusable and is still part of the document flow.
## Accessibility
Native `readOnly` + `aria-readonly="true"`. The control is focusable so users can read its state; `preventDefault` keeps the value fixed. 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 checkbox uses the semantic color, radius, spacing, and motion tokens.
## Notes
Use this for a value that is fixed in this context but should still be focusable and perceivable (e.g. a system-managed permission). Use `checkbox-disabled` when the option is genuinely non-interactive. 90 lines UTF-8 · LF · Spaces: 2
Continue browsing