shadimage

Checkbox

registry/pouf/checkbox.tsx

import * as RCheck from '@radix-ui/react-checkbox'
import clsx from 'clsx'
import { useId } from 'react'

interface CheckboxProps {
  checked: boolean | 'indeterminate'
  onChange: (checked: boolean | 'indeterminate') => void
  id?: string
  disabled?: boolean
  label?: string
  /** Keep `label` as the accessible name but stop rendering it visibly.
   *  For rows that already display the text themselves — without this, the
   *  only way to give the box an accessible name was to also print a second
   *  copy of the text next to it. Opt-in, so existing call sites are
   *  unchanged. */
  hideLabel?: boolean
}

export function Checkbox({ checked, onChange, id, disabled, label, hideLabel }: CheckboxProps) {
  const generatedId = useId()
  const controlId = id ?? generatedId
  return (
    <div className="pouf-checkbox-row inline-flex items-center gap-(--s2)">
      <RCheck.Root
        id={controlId}
        className={clsx([
          'pouf-checkbox w-7 h-7 rounded-[9px] border-none p-0 cursor-pointer cushion-field flex-none',
          'flex items-center justify-center',
          '[transition:background_160ms_ease,transform_160ms_cubic-bezier(0.23,1,0.32,1)]',
          'enabled:active:[transform:scale(0.92)]',
          'data-[state=checked]:bg-purple disabled:opacity-50 disabled:cursor-not-allowed',
        ],
        checked === 'indeterminate' ? 'bg-purple' : 'bg-bg')}
        // Pass the real tri-state to Radix. Passing `checked === true` collapsed
        // indeterminate to false, so Radix hid the Indicator entirely and the box
        // painted a blank purple blob with no dash. Radix renders the Indicator for
        // 'indeterminate' too, so the minus glyph below now shows.
        checked={checked === 'indeterminate' ? 'indeterminate' : checked}
        onCheckedChange={onChange}
        disabled={disabled}
        aria-label={label}
      >
        <RCheck.Indicator className={[
            'pouf-checkbox__indicator text-[var(--on-accent)] flex [&_svg]:w-5 [&_svg]:h-5',
            /* Radix mounts the indicator on check, so animate that HTML wrapper
             * with compositor-friendly properties instead of animating the SVG
             * path's stroke. Reduced motion keeps a short opacity-only fade. */
            '[animation:pouf-check-pop_250ms_cubic-bezier(0.23,1,0.32,1)_both]',
            'motion-reduce:[animation-name:pouf-check-fade]',
          ].join(' ')}>
          {/* Inline rather than Icon so the indeterminate state can swap the
              stroke while keeping geometry consistent with the Tabler set. */}
          <svg
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth={2.4}
            strokeLinecap="round"
            strokeLinejoin="round"
            aria-hidden="true"
          >
            {checked === 'indeterminate' ? (
              <path d="M5 12h14" />
            ) : (
              <path d="M5 12l5 5L20 7" />
            )}
          </svg>
        </RCheck.Indicator>
      </RCheck.Root>
      {label && !hideLabel && (
        <label htmlFor={controlId} className="pouf-checkbox__label text-[15px] font-bold text-ink cursor-pointer">
          {label}
        </label>
      )}
    </div>
  )
}

No selection tags yet — empty means no evidence, not lack of support.

DependenciesBundle weight · light

ui-primitives
@radix-ui/react-checkbox