niceui

Checkbox

Checkboxes whose tick draws itself with an SVG stroke, plus an indeterminate state for select-all groups.

ReactTailwind CSSTypeScriptComponentsMIT · Free
Live preview

Installation

01

Install with the shadcn CLI. The NiceUI theme comes along the first time.

$ npx shadcn@latest add https://niceui.dev/r/components-checkbox.json
02

Or copy the source below. Dependencies: none.

Source

components/niceui/ui/core/Checkbox.tsx
import { useId, useState, type ReactNode } from 'react';
import { cn } from '@/lib/cn';

export type CheckboxProps = {
  checked: boolean;
  onChange: (checked: boolean) => void;
  indeterminate?: boolean;
  label?: ReactNode;
  description?: ReactNode;
  disabled?: boolean;
  className?: string;
};

export function Checkbox({ checked, onChange, indeterminate = false, label, description, disabled, className }: CheckboxProps) {
  const id = useId();
  const on = checked || indeterminate;

  return (
    <label
      htmlFor={id}
      className={cn('group inline-flex cursor-pointer items-start gap-3', disabled && 'cursor-not-allowed opacity-50', className)}
    >
      <span className="relative mt-0.5 flex h-[18px] w-[18px] shrink-0">
        <input
          id={id}
          type="checkbox"
          className="peer absolute inset-0 cursor-[inherit] appearance-none rounded-[5px] outline-none"
          checked={checked}
          disabled={disabled}
          aria-checked={indeterminate ? 'mixed' : checked}
          onChange={(e) => onChange(e.target.checked)}
        />
        <span
          aria-hidden
          className={cn(
            'pointer-events-none flex h-full w-full items-center justify-center rounded-[5px] transition-all duration-200 ease-smooth group-active:scale-90 peer-focus-visible:ring-2 peer-focus-visible:ring-brand-500/60 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-ink-950',
            on
              ? 'bg-brand-500 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.15),0_0_12px_-2px_rgba(56,189,248,0.6)]'
              : 'bg-ink-900 shadow-[inset_0_0_0_1.5px_rgba(255,255,255,0.18)] group-hover:shadow-[inset_0_0_0_1.5px_rgba(255,255,255,0.32)]',
          )}
        >
          <svg viewBox="0 0 16 16" className="h-3.5 w-3.5" fill="none" stroke="white" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round">
            <path
              d="M3.5 8.5 6.5 11.5 12.5 4.5"
              strokeDasharray={16}
              strokeDashoffset={checked && !indeterminate ? 0 : 16}
              style={{ transition: `stroke-dashoffset ${checked && !indeterminate ? '350ms cubic-bezier(0.65,0,0.35,1) 60ms' : '150ms ease-in'}` }}
            />
            <path
              d="M4 8h8"
              strokeDasharray={8}
              strokeDashoffset={indeterminate ? 0 : 8}
              style={{ transition: 'stroke-dashoffset 250ms cubic-bezier(0.65,0,0.35,1)' }}
            />
          </svg>
        </span>
      </span>
      {(label || description) && (
        <span className="select-none">
          {label && <span className="block text-sm font-medium text-ink-100">{label}</span>}
          {description && <span className="block text-xs text-ink-400">{description}</span>}
        </span>
      )}
    </label>
  );
}

const PERMS = [
  { id: 'read', label: 'Read deployments', desc: 'View builds, logs and previews.' },
  { id: 'write', label: 'Create deployments', desc: 'Push to preview and production.' },
  { id: 'env', label: 'Manage environment variables', desc: 'Read and edit secrets.' },
  { id: 'domains', label: 'Configure domains', desc: 'Add, verify and remove domains.' },
  { id: 'billing', label: 'Billing access', desc: 'View invoices and change plan.' },
];

export function CheckboxDemo() {
  const [sel, setSel] = useState<string[]>(['read', 'write']);
  const all = sel.length === PERMS.length;
  const some = sel.length > 0 && !all;

  return (
    <div className="mx-auto w-full max-w-sm overflow-hidden rounded-2xl bg-ink-900 shadow-card">
      <div className="flex items-center justify-between border-b border-white/5 bg-ink-950/40 px-4 py-3">
        <Checkbox
          checked={all}
          indeterminate={some}
          onChange={() => setSel(all ? [] : PERMS.map((p) => p.id))}
          label="API token scopes"
        />
        <span className="text-xs tabular-nums text-ink-400">
          {sel.length}/{PERMS.length}
        </span>
      </div>
      <div className="space-y-1 p-2">
        {PERMS.map((p, i) => (
          <div
            key={p.id}
            className={cn('animate-fade-up rounded-lg px-2 py-2 transition-colors', sel.includes(p.id) ? 'bg-brand-500/[0.06]' : 'hover:bg-white/[0.03]')}
            style={{ animationDelay: `${i * 50}ms` }}
          >
            <Checkbox
              checked={sel.includes(p.id)}
              onChange={(c) => setSel((s) => (c ? [...s, p.id] : s.filter((x) => x !== p.id)))}
              label={p.label}
              description={p.desc}
              className="w-full"
            />
          </div>
        ))}
      </div>
    </div>
  );
}