shadimage

Checkbox

Accessible checkbox with indeterminate state for Tile UI.

packages/react/src/components/checkbox/checkbox.tsx

import React, { useState } from 'react';
import { checkboxStyleKeys, getCheckboxState, getNextCheckboxState } from '../lib/core';
import type { CheckboxCheckedState } from '../lib/core';
import styles from './checkbox.module.scss';

export interface CheckboxProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange' | 'defaultChecked'> {
	checked?: CheckboxCheckedState;
	defaultChecked?: CheckboxCheckedState;
	onCheckedChange?: (checked: CheckboxCheckedState) => void;
}

const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(({ className = '', checked, defaultChecked = false, disabled, onCheckedChange, ...props }, ref) => {
	const [internalChecked, setInternalChecked] = useState<CheckboxCheckedState>(defaultChecked);
	const isChecked = checked !== undefined ? checked : internalChecked;
	const state = getCheckboxState(isChecked);

	function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
		const next = getNextCheckboxState(isChecked);

		if (checked === undefined) {
			setInternalChecked(next);
		}

		onCheckedChange?.(next);
		props.onClick?.(event);
	}

	const ariaChecked = state === 'checked' ? 'true' : state === 'mixed' ? 'mixed' : 'false';

	return (
		<button
			ref={ref}
			type="button"
			role="checkbox"
			aria-checked={ariaChecked}
			data-state={state}
			disabled={disabled}
			className={`${styles[checkboxStyleKeys.root]} ${className}`}
			{...props}
			onClick={handleClick}>
			<span className={styles[checkboxStyleKeys.indicator]}>
				{state === 'checked' && (
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
						<path d="M20 6 9 17l-5-5" />
					</svg>
				)}
				{state === 'mixed' && (
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round">
						<path d="M5 12h14" />
					</svg>
				)}
			</span>
		</button>
	);
});

Checkbox.displayName = 'Checkbox';

export { Checkbox };
export default Checkbox;
Where it goes
stats
Interaction
animated-counter