diff --git a/apps/www/src/content/docs/components/checkbox/demo.ts b/apps/www/src/content/docs/components/checkbox/demo.ts
index e57be2af5..23d700cb2 100644
--- a/apps/www/src/content/docs/components/checkbox/demo.ts
+++ b/apps/www/src/content/docs/components/checkbox/demo.ts
@@ -25,6 +25,11 @@ export const playground = {
disabled: {
type: 'checkbox',
defaultValue: false
+ },
+ size: {
+ type: 'select',
+ options: ['large', 'small'],
+ defaultValue: 'large'
}
},
getCode
@@ -52,6 +57,20 @@ export const statesExamples = {
]
};
+export const sizeExamples = {
+ type: 'code',
+ tabs: [
+ {
+ name: 'Large (default)',
+ code: ``
+ },
+ {
+ name: 'Small',
+ code: ``
+ }
+ ]
+};
+
export const groupDemo = {
type: 'code',
code: `
@@ -73,6 +92,25 @@ export const groupDemo = {
`
};
+export const groupHorizontalDemo = {
+ type: 'code',
+ code: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+};
+
export const groupDisabledDemo = {
type: 'code',
code: `
diff --git a/apps/www/src/content/docs/components/checkbox/index.mdx b/apps/www/src/content/docs/components/checkbox/index.mdx
index df9b952fd..6aed9fa7a 100644
--- a/apps/www/src/content/docs/components/checkbox/index.mdx
+++ b/apps/www/src/content/docs/components/checkbox/index.mdx
@@ -4,7 +4,7 @@ description: Checkbox is a user interface control that enables users to toggle b
source: packages/raystack/components/checkbox
---
-import { playground, statesExamples, groupDemo, groupDisabledDemo, parentDemo } from "./demo.ts";
+import { playground, statesExamples, sizeExamples, groupDemo, groupHorizontalDemo, groupDisabledDemo, parentDemo } from "./demo.ts";
@@ -55,12 +55,24 @@ The Checkbox component supports multiple states to represent different selection
+### Size Variants
+
+The Checkbox component comes in two sizes: `large` (default) and `small`.
+
+
+
### Group
Use `Checkbox.Group` to coordinate multiple checkboxes with shared state.
+### Horizontal Group
+
+Use the `orientation` prop to lay out checkboxes in a horizontal row.
+
+
+
### Disabled Group
Disable the entire group to prevent user interaction.
@@ -80,3 +92,6 @@ Use a parent checkbox with `allValues` to toggle all items at once.
- Uses `aria-checked` to indicate state (checked, unchecked, indeterminate)
- Associates with labels via `id` and `htmlFor` attributes
- Wrap `Checkbox.Group` with `aria-label` or `aria-labelledby` for an accessible group name
+- **Disabled state** preserves the visual checked/indeterminate appearance while preventing interaction
+- **Read-only state** reduces opacity and changes cursor to indicate non-editable content
+- **Invalid state** displays a danger border (or danger background when checked/indeterminate) for form validation feedback
diff --git a/apps/www/src/content/docs/components/checkbox/props.ts b/apps/www/src/content/docs/components/checkbox/props.ts
index caba71e94..1d40d3887 100644
--- a/apps/www/src/content/docs/components/checkbox/props.ts
+++ b/apps/www/src/content/docs/components/checkbox/props.ts
@@ -20,10 +20,24 @@ export interface CheckboxProps {
indeterminate?: boolean;
/**
- * When true, prevents the user from interacting with the checkbox
+ * When true, prevents the user from interacting with the checkbox.
+ * @defaultValue false
*/
disabled?: boolean;
+ /**
+ * When true, the checkbox is displayed in a read-only state.
+ * The user cannot change the value but it is still focusable.
+ * @defaultValue false
+ */
+ readOnly?: boolean;
+
+ /**
+ * When true, the user must tick the checkbox before submitting a form.
+ * @defaultValue false
+ */
+ required?: boolean;
+
/**
* Identifies the checkbox within a `Checkbox.Group`. The group uses this value in its `value` array.
*/
@@ -35,6 +49,20 @@ export interface CheckboxProps {
*/
parent?: boolean;
+ /**
+ * The size of the checkbox.
+ * @defaultValue 'large'
+ */
+ size?: 'small' | 'large';
+
+ /**
+ * Custom render function for the indicator. Receives `(props, state)` where state includes `checked` and `indeterminate`.
+ */
+ render?: (
+ props: React.HTMLAttributes,
+ state: { checked: boolean; indeterminate: boolean }
+ ) => React.ReactNode;
+
/** Additional CSS class name. */
className?: string;
}
@@ -60,6 +88,12 @@ export interface CheckboxGroupProps {
*/
disabled?: boolean;
+ /**
+ * Layout direction of the checkbox group.
+ * @defaultValue 'vertical'
+ */
+ orientation?: 'vertical' | 'horizontal';
+
/** Additional CSS class name. */
className?: string;
}
diff --git a/packages/raystack/components/checkbox/__tests__/checkbox-group.test.tsx b/packages/raystack/components/checkbox/__tests__/checkbox-group.test.tsx
index 3d6e5468c..609e97b63 100644
--- a/packages/raystack/components/checkbox/__tests__/checkbox-group.test.tsx
+++ b/packages/raystack/components/checkbox/__tests__/checkbox-group.test.tsx
@@ -63,6 +63,44 @@ describe('Checkbox.Group', () => {
});
});
+ describe('Orientation', () => {
+ it('renders vertical by default', () => {
+ render(
+
+
+
+
+ );
+ const group = screen.getByTestId('group');
+ expect(group).toHaveClass(styles.group);
+ expect(group).not.toHaveClass(styles['group-horizontal']);
+ });
+
+ it('renders vertical when orientation="vertical"', () => {
+ render(
+
+
+
+
+ );
+ const group = screen.getByTestId('group');
+ expect(group).toHaveClass(styles.group);
+ expect(group).not.toHaveClass(styles['group-horizontal']);
+ });
+
+ it('renders horizontal when orientation="horizontal"', () => {
+ render(
+
+
+
+
+ );
+ const group = screen.getByTestId('group');
+ expect(group).toHaveClass(styles.group);
+ expect(group).toHaveClass(styles['group-horizontal']);
+ });
+ });
+
describe('Selection Behavior', () => {
it('works with defaultValue', () => {
render(
diff --git a/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx b/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx
index eec6d6147..cb4fb60f9 100644
--- a/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx
+++ b/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx
@@ -31,6 +31,23 @@ describe('Checkbox', () => {
});
});
+ describe('Sizes', () => {
+ const sizes = ['small', 'large'] as const;
+ sizes.forEach(size => {
+ it(`renders ${size} size`, () => {
+ render();
+ const checkbox = screen.getByRole('checkbox');
+ expect(checkbox).toHaveClass(styles[size]);
+ });
+ });
+
+ it('renders large size by default', () => {
+ render();
+ const checkbox = screen.getByRole('checkbox');
+ expect(checkbox).toHaveClass(styles.large);
+ });
+ });
+
describe('Checked State', () => {
it('renders unchecked by default', () => {
render();
@@ -133,6 +150,52 @@ describe('Checkbox', () => {
});
});
+ describe('Custom Indicator (render prop)', () => {
+ it('renders custom indicator via render prop', () => {
+ render(
+ (
+
+ {state.checked ? 'checked' : 'unchecked'}
+
+ )}
+ />
+ );
+ expect(screen.getByTestId('custom-indicator')).toHaveTextContent(
+ 'checked'
+ );
+ });
+
+ it('receives indeterminate state in render prop', () => {
+ render(
+ (
+
+ {state.indeterminate ? 'mixed' : 'clear'}
+
+ )}
+ />
+ );
+ expect(screen.getByTestId('custom-indicator')).toHaveTextContent('mixed');
+ });
+
+ it('renders default check icon when no render prop provided', () => {
+ const { container } = render();
+ const indicator = container.querySelector(`.${styles.indicator}`);
+ const svg = indicator?.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ });
+
+ it('renders default indeterminate icon when no render prop provided', () => {
+ const { container } = render();
+ const indicator = container.querySelector(`.${styles.indicator}`);
+ const svg = indicator?.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ });
+ });
+
describe('Event Handling', () => {
it('calls onCheckedChange when clicked', () => {
const handleChange = vi.fn();
@@ -232,5 +295,11 @@ describe('Checkbox', () => {
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('aria-invalid', 'true');
});
+
+ it('supports required prop', () => {
+ render();
+ const checkbox = screen.getByRole('checkbox');
+ expect(checkbox).toHaveAttribute('data-required');
+ });
});
});
diff --git a/packages/raystack/components/checkbox/checkbox.module.css b/packages/raystack/components/checkbox/checkbox.module.css
index d4b538a99..a12fe9aa8 100644
--- a/packages/raystack/components/checkbox/checkbox.module.css
+++ b/packages/raystack/components/checkbox/checkbox.module.css
@@ -4,16 +4,17 @@
gap: var(--rs-space-3);
}
+.group-horizontal {
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
.checkbox {
all: unset;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
- width: var(--rs-space-5);
- height: var(--rs-space-5);
- min-width: var(--rs-space-5);
- min-height: var(--rs-space-5);
border-radius: var(--rs-radius-1);
background: var(--rs-color-background-base-primary);
border: 1px solid var(--rs-color-border-base-secondary);
@@ -21,6 +22,21 @@
flex-shrink: 0;
}
+/* Size variants */
+.large {
+ width: var(--rs-space-5);
+ height: var(--rs-space-5);
+ min-width: var(--rs-space-5);
+ min-height: var(--rs-space-5);
+}
+
+.small {
+ width: var(--rs-space-4);
+ height: var(--rs-space-4);
+ min-width: var(--rs-space-4);
+ min-height: var(--rs-space-4);
+}
+
.checkbox:hover {
background: var(--rs-color-background-base-primary-hover);
border-color: var(--rs-color-border-base-focus);
@@ -41,18 +57,48 @@
border: none;
}
+/* A5: Indeterminate hover visual feedback */
.checkbox[data-indeterminate]:hover {
- background: var(--rs-color-background-neutral-tertiary);
+ background: var(--rs-color-background-neutral-secondary);
border: none;
}
+/* A2: Read-only styling */
+.checkbox[data-readonly] {
+ cursor: default;
+ opacity: 0.7;
+}
+
+/* A3: Invalid state — red border for validation errors */
+.checkbox[data-invalid] {
+ border-color: var(--rs-color-border-danger-primary);
+}
+
+.checkbox[data-invalid][data-checked],
+.checkbox[data-invalid][data-indeterminate] {
+ background: var(--rs-color-background-danger-primary);
+ border-color: var(--rs-color-border-danger-primary);
+}
+
+/* A4: Disabled state */
.checkbox[data-disabled] {
opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
}
-.checkbox[data-disabled]:hover {
- background: var(--rs-color-background-base-primary);
- border-color: var(--rs-color-border-base-primary);
+/* A4: Preserve checked state when disabled */
+.checkbox[data-disabled][data-checked],
+.checkbox[data-disabled][data-checked]:hover {
+ background: var(--rs-color-background-accent-emphasis);
+ border: none;
+}
+
+/* A4: Preserve indeterminate state when disabled */
+.checkbox[data-disabled][data-indeterminate],
+.checkbox[data-disabled][data-indeterminate]:hover {
+ background: var(--rs-color-background-neutral-tertiary);
+ border: none;
}
.indicator {
@@ -65,6 +111,6 @@
}
.icon {
- width: var(--rs-space-5);
- height: var(--rs-space-5);
-}
\ No newline at end of file
+ width: 100%;
+ height: 100%;
+}
diff --git a/packages/raystack/components/checkbox/checkbox.tsx b/packages/raystack/components/checkbox/checkbox.tsx
index c8e7dd3d5..d3b9340fe 100644
--- a/packages/raystack/components/checkbox/checkbox.tsx
+++ b/packages/raystack/components/checkbox/checkbox.tsx
@@ -2,7 +2,7 @@
import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox';
import { CheckboxGroup as CheckboxGroupPrimitive } from '@base-ui/react/checkbox-group';
-import { cx } from 'class-variance-authority';
+import { cva, cx, type VariantProps } from 'class-variance-authority';
import { useFieldContext } from '../field';
import styles from './checkbox.module.css';
@@ -43,10 +43,37 @@ const IndeterminateIcon = () => (
);
-function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) {
+const checkboxVariants = cva(styles.checkbox, {
+ variants: {
+ size: {
+ small: styles.small,
+ large: styles.large
+ }
+ },
+ defaultVariants: {
+ size: 'large'
+ }
+});
+
+interface CheckboxGroupProps extends CheckboxGroupPrimitive.Props {
+ /** Layout direction of the checkbox group.
+ * @defaultValue 'vertical'
+ */
+ orientation?: 'vertical' | 'horizontal';
+}
+
+function CheckboxGroup({
+ className,
+ orientation = 'vertical',
+ ...props
+}: CheckboxGroupProps) {
return (
);
@@ -54,27 +81,36 @@ function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) {
CheckboxGroup.displayName = 'Checkbox.Group';
+interface CheckboxItemProps
+ extends CheckboxPrimitive.Root.Props,
+ VariantProps {}
+
function CheckboxItem({
className,
required,
+ size,
+ render,
...props
-}: CheckboxPrimitive.Root.Props) {
+}: CheckboxItemProps) {
const fieldContext = useFieldContext();
const resolvedRequired = required ?? fieldContext?.required;
return (
(
-
- {state.indeterminate ? : }
-
- )}
+ render={
+ render ??
+ ((props, state) => (
+
+ {state.indeterminate ? : }
+
+ ))
+ }
/>
);