From db22d7f04ddb6bdc2fb1ba5acb2d0e79a3436d39 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 15 Apr 2026 01:54:53 +0530 Subject: [PATCH 1/4] feat(checkbox): add accessibility features, size variants, custom icons, and group orientation Add focus-visible ring (WCAG 2.4.7), readOnly styling, invalid/required state styling, fix disabled+checked hover bug, add indeterminate hover feedback, size variants (small/large), custom icon props (checkedIcon, indeterminateIcon), explicit parent/inputRef prop types, and CheckboxGroup orientation prop (vertical/horizontal). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../content/docs/components/checkbox/demo.ts | 38 ++++++++++ .../content/docs/components/checkbox/props.ts | 27 +++++++ .../__tests__/checkbox-group.test.tsx | 38 ++++++++++ .../checkbox/__tests__/checkbox.test.tsx | 61 +++++++++++++++ .../components/checkbox/checkbox.module.css | 74 ++++++++++++++++--- .../raystack/components/checkbox/checkbox.tsx | 58 +++++++++++++-- 6 files changed, 279 insertions(+), 17 deletions(-) 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/props.ts b/apps/www/src/content/docs/components/checkbox/props.ts index caba71e94..443d8037c 100644 --- a/apps/www/src/content/docs/components/checkbox/props.ts +++ b/apps/www/src/content/docs/components/checkbox/props.ts @@ -35,6 +35,27 @@ export interface CheckboxProps { */ parent?: boolean; + /** + * The size of the checkbox. + * @defaultValue 'large' + */ + size?: 'small' | 'large'; + + /** + * Custom icon to render when the checkbox is checked. + */ + checkedIcon?: React.ReactNode; + + /** + * Custom icon to render when the checkbox is in the indeterminate state. + */ + indeterminateIcon?: React.ReactNode; + + /** + * Ref to the hidden `` element for form integration. + */ + inputRef?: React.Ref; + /** Additional CSS class name. */ className?: string; } @@ -60,6 +81,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..a65471b57 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,44 @@ describe('Checkbox', () => { }); }); + describe('Custom Icons', () => { + it('renders custom checked icon when provided', () => { + render( + custom} + /> + ); + expect(screen.getByTestId('custom-check')).toBeInTheDocument(); + }); + + it('renders custom indeterminate icon when provided', () => { + render( + custom + } + /> + ); + expect(screen.getByTestId('custom-indeterminate')).toBeInTheDocument(); + }); + + it('renders default check icon when no custom icon is 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 custom icon is 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 +287,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..e6b02f878 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,27 @@ 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); +} + +/* A1: Focus-visible ring for keyboard navigation (WCAG 2.4.7) */ +.checkbox:focus-visible { + outline: 2px solid var(--rs-color-border-accent-emphasis); + outline-offset: 2px; +} + .checkbox:hover { background: var(--rs-color-background-base-primary-hover); border-color: var(--rs-color-border-base-focus); @@ -41,18 +63,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 +117,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..bda9e21bd 100644 --- a/packages/raystack/components/checkbox/checkbox.tsx +++ b/packages/raystack/components/checkbox/checkbox.tsx @@ -2,7 +2,8 @@ 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 { ReactNode } from 'react'; import { useFieldContext } from '../field'; import styles from './checkbox.module.css'; @@ -43,10 +44,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,17 +82,33 @@ function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) { CheckboxGroup.displayName = 'Checkbox.Group'; +interface CheckboxItemProps + extends CheckboxPrimitive.Root.Props, + VariantProps { + /** Custom icon to render when the checkbox is checked. */ + checkedIcon?: ReactNode; + /** Custom icon to render when the checkbox is in the indeterminate state. */ + indeterminateIcon?: ReactNode; + /** Ref to the hidden element for form integration. */ + inputRef?: React.Ref; + /** When true, the checkbox acts as a parent (select all) checkbox within a group. */ + parent?: boolean; +} + function CheckboxItem({ className, required, + size, + checkedIcon, + indeterminateIcon, ...props -}: CheckboxPrimitive.Root.Props) { +}: CheckboxItemProps) { const fieldContext = useFieldContext(); const resolvedRequired = required ?? fieldContext?.required; return ( @@ -72,7 +116,9 @@ function CheckboxItem({ className={styles.indicator} render={(props, state) => ( - {state.indeterminate ? : } + {state.indeterminate + ? (indeterminateIcon ?? ) + : (checkedIcon ?? )} )} /> From 76f664ad08d410e9d28e815d29b4ac23d0606bdb Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 15 Apr 2026 02:42:21 +0530 Subject: [PATCH 2/4] refactor(checkbox): use children prop for custom icons, remove focus-visible ring - Remove focus-visible CSS (cross-cutting concern, not checkbox-specific) - Replace checkedIcon/indeterminateIcon props with children prop that accepts ReactNode or render function receiving { checked, indeterminate } - Remove explicit inputRef/parent from CheckboxItemProps (already exposed via Base UI's CheckboxPrimitive.Root.Props) - Update tests and docs accordingly Co-Authored-By: Claude Opus 4.6 (1M context) --- .../content/docs/components/checkbox/props.ts | 19 +++----- .../checkbox/__tests__/checkbox.test.tsx | 47 ++++++++++++------- .../components/checkbox/checkbox.module.css | 6 --- .../raystack/components/checkbox/checkbox.tsx | 34 ++++++++------ 4 files changed, 57 insertions(+), 49 deletions(-) diff --git a/apps/www/src/content/docs/components/checkbox/props.ts b/apps/www/src/content/docs/components/checkbox/props.ts index 443d8037c..4419b3e40 100644 --- a/apps/www/src/content/docs/components/checkbox/props.ts +++ b/apps/www/src/content/docs/components/checkbox/props.ts @@ -42,19 +42,14 @@ export interface CheckboxProps { size?: 'small' | 'large'; /** - * Custom icon to render when the checkbox is checked. + * Custom indicator content. Can be a ReactNode or a function receiving `{ checked, indeterminate }` state. */ - checkedIcon?: React.ReactNode; - - /** - * Custom icon to render when the checkbox is in the indeterminate state. - */ - indeterminateIcon?: React.ReactNode; - - /** - * Ref to the hidden `` element for form integration. - */ - inputRef?: React.Ref; + children?: + | React.ReactNode + | ((state: { + checked: boolean; + indeterminate: boolean; + }) => React.ReactNode); /** Additional CSS class name. */ className?: string; diff --git a/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx b/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx index a65471b57..b7d39d567 100644 --- a/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx +++ b/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx @@ -150,37 +150,50 @@ describe('Checkbox', () => { }); }); - describe('Custom Icons', () => { - it('renders custom checked icon when provided', () => { + describe('Children (Custom Icons)', () => { + it('renders children as ReactNode when provided', () => { render( - custom} - /> + + custom + ); - expect(screen.getByTestId('custom-check')).toBeInTheDocument(); + expect(screen.getByTestId('custom-icon')).toBeInTheDocument(); }); - it('renders custom indeterminate icon when provided', () => { + it('renders children as render function with state', () => { render( - custom - } - /> + + {({ checked, indeterminate }) => ( + + {checked ? 'yes' : 'no'}-{indeterminate ? 'mixed' : 'clear'} + + )} + ); - expect(screen.getByTestId('custom-indeterminate')).toBeInTheDocument(); + expect(screen.getByTestId('render-fn')).toHaveTextContent('yes-clear'); }); - it('renders default check icon when no custom icon is provided', () => { + it('renders render function with indeterminate state', () => { + render( + + {({ indeterminate }) => ( + + {indeterminate ? 'mixed' : 'clear'} + + )} + + ); + expect(screen.getByTestId('render-fn')).toHaveTextContent('mixed'); + }); + + it('renders default check icon when no children 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 custom icon is provided', () => { + it('renders default indeterminate icon when no children provided', () => { const { container } = render(); const indicator = container.querySelector(`.${styles.indicator}`); const svg = indicator?.querySelector('svg'); diff --git a/packages/raystack/components/checkbox/checkbox.module.css b/packages/raystack/components/checkbox/checkbox.module.css index e6b02f878..a12fe9aa8 100644 --- a/packages/raystack/components/checkbox/checkbox.module.css +++ b/packages/raystack/components/checkbox/checkbox.module.css @@ -37,12 +37,6 @@ min-height: var(--rs-space-4); } -/* A1: Focus-visible ring for keyboard navigation (WCAG 2.4.7) */ -.checkbox:focus-visible { - outline: 2px solid var(--rs-color-border-accent-emphasis); - outline-offset: 2px; -} - .checkbox:hover { background: var(--rs-color-background-base-primary-hover); border-color: var(--rs-color-border-base-focus); diff --git a/packages/raystack/components/checkbox/checkbox.tsx b/packages/raystack/components/checkbox/checkbox.tsx index bda9e21bd..ca27e154e 100644 --- a/packages/raystack/components/checkbox/checkbox.tsx +++ b/packages/raystack/components/checkbox/checkbox.tsx @@ -3,7 +3,7 @@ import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'; import { CheckboxGroup as CheckboxGroupPrimitive } from '@base-ui/react/checkbox-group'; import { cva, cx, type VariantProps } from 'class-variance-authority'; -import { ReactNode } from 'react'; +import { type ReactNode } from 'react'; import { useFieldContext } from '../field'; import styles from './checkbox.module.css'; @@ -85,22 +85,17 @@ CheckboxGroup.displayName = 'Checkbox.Group'; interface CheckboxItemProps extends CheckboxPrimitive.Root.Props, VariantProps { - /** Custom icon to render when the checkbox is checked. */ - checkedIcon?: ReactNode; - /** Custom icon to render when the checkbox is in the indeterminate state. */ - indeterminateIcon?: ReactNode; - /** Ref to the hidden element for form integration. */ - inputRef?: React.Ref; - /** When true, the checkbox acts as a parent (select all) checkbox within a group. */ - parent?: boolean; + /** Custom indicator content. Can be a ReactNode or a function receiving the checkbox state. */ + children?: + | ReactNode + | ((state: { checked: boolean; indeterminate: boolean }) => ReactNode); } function CheckboxItem({ className, required, size, - checkedIcon, - indeterminateIcon, + children, ...props }: CheckboxItemProps) { const fieldContext = useFieldContext(); @@ -116,9 +111,20 @@ function CheckboxItem({ className={styles.indicator} render={(props, state) => ( - {state.indeterminate - ? (indeterminateIcon ?? ) - : (checkedIcon ?? )} + {children ? ( + typeof children === 'function' ? ( + children({ + checked: state.checked, + indeterminate: state.indeterminate + }) + ) : ( + children + ) + ) : state.indeterminate ? ( + + ) : ( + + )} )} /> From 22682dcd92439d71dc384c15c4723b435350dc87 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 15 Apr 2026 02:49:45 +0530 Subject: [PATCH 3/4] refactor(checkbox): use render prop instead of children for custom indicator Extract render from checkbox props and pass it to the Indicator primitive. When no render prop is provided, falls back to default CheckMarkIcon/IndeterminateIcon. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../content/docs/components/checkbox/props.ts | 12 ++--- .../checkbox/__tests__/checkbox.test.tsx | 47 +++++++++---------- .../raystack/components/checkbox/checkbox.tsx | 36 ++++---------- 3 files changed, 36 insertions(+), 59 deletions(-) diff --git a/apps/www/src/content/docs/components/checkbox/props.ts b/apps/www/src/content/docs/components/checkbox/props.ts index 4419b3e40..6e77de429 100644 --- a/apps/www/src/content/docs/components/checkbox/props.ts +++ b/apps/www/src/content/docs/components/checkbox/props.ts @@ -42,14 +42,12 @@ export interface CheckboxProps { size?: 'small' | 'large'; /** - * Custom indicator content. Can be a ReactNode or a function receiving `{ checked, indeterminate }` state. + * Custom render function for the indicator. Receives `(props, state)` where state includes `checked` and `indeterminate`. */ - children?: - | React.ReactNode - | ((state: { - checked: boolean; - indeterminate: boolean; - }) => React.ReactNode); + render?: ( + props: React.HTMLAttributes, + state: { checked: boolean; indeterminate: boolean } + ) => React.ReactNode; /** Additional CSS class name. */ className?: string; diff --git a/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx b/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx index b7d39d567..cb4fb60f9 100644 --- a/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx +++ b/packages/raystack/components/checkbox/__tests__/checkbox.test.tsx @@ -150,50 +150,45 @@ describe('Checkbox', () => { }); }); - describe('Children (Custom Icons)', () => { - it('renders children as ReactNode when provided', () => { + describe('Custom Indicator (render prop)', () => { + it('renders custom indicator via render prop', () => { render( - - custom - - ); - expect(screen.getByTestId('custom-icon')).toBeInTheDocument(); - }); - - it('renders children as render function with state', () => { - render( - - {({ checked, indeterminate }) => ( - - {checked ? 'yes' : 'no'}-{indeterminate ? 'mixed' : 'clear'} + ( + + {state.checked ? 'checked' : 'unchecked'} )} - + /> + ); + expect(screen.getByTestId('custom-indicator')).toHaveTextContent( + 'checked' ); - expect(screen.getByTestId('render-fn')).toHaveTextContent('yes-clear'); }); - it('renders render function with indeterminate state', () => { + it('receives indeterminate state in render prop', () => { render( - - {({ indeterminate }) => ( - - {indeterminate ? 'mixed' : 'clear'} + ( + + {state.indeterminate ? 'mixed' : 'clear'} )} - + /> ); - expect(screen.getByTestId('render-fn')).toHaveTextContent('mixed'); + expect(screen.getByTestId('custom-indicator')).toHaveTextContent('mixed'); }); - it('renders default check icon when no children provided', () => { + 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 children provided', () => { + it('renders default indeterminate icon when no render prop provided', () => { const { container } = render(); const indicator = container.querySelector(`.${styles.indicator}`); const svg = indicator?.querySelector('svg'); diff --git a/packages/raystack/components/checkbox/checkbox.tsx b/packages/raystack/components/checkbox/checkbox.tsx index ca27e154e..d3b9340fe 100644 --- a/packages/raystack/components/checkbox/checkbox.tsx +++ b/packages/raystack/components/checkbox/checkbox.tsx @@ -3,7 +3,6 @@ import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'; import { CheckboxGroup as CheckboxGroupPrimitive } from '@base-ui/react/checkbox-group'; import { cva, cx, type VariantProps } from 'class-variance-authority'; -import { type ReactNode } from 'react'; import { useFieldContext } from '../field'; import styles from './checkbox.module.css'; @@ -84,18 +83,13 @@ CheckboxGroup.displayName = 'Checkbox.Group'; interface CheckboxItemProps extends CheckboxPrimitive.Root.Props, - VariantProps { - /** Custom indicator content. Can be a ReactNode or a function receiving the checkbox state. */ - children?: - | ReactNode - | ((state: { checked: boolean; indeterminate: boolean }) => ReactNode); -} + VariantProps {} function CheckboxItem({ className, required, size, - children, + render, ...props }: CheckboxItemProps) { const fieldContext = useFieldContext(); @@ -109,24 +103,14 @@ function CheckboxItem({ > ( - - {children ? ( - typeof children === 'function' ? ( - children({ - checked: state.checked, - indeterminate: state.indeterminate - }) - ) : ( - children - ) - ) : state.indeterminate ? ( - - ) : ( - - )} - - )} + render={ + render ?? + ((props, state) => ( + + {state.indeterminate ? : } + + )) + } /> ); From 3cfff8ab9456b93386f4f7060127350545a13a57 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 16 Apr 2026 02:34:52 +0530 Subject: [PATCH 4/4] docs(checkbox): add missing size variants, horizontal group, and accessibility props The docs were missing sections for size variants and horizontal group orientation that were added in the component. Also documents readOnly, required props and the new CSS-driven accessibility states (disabled, read-only, invalid visual feedback). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../content/docs/components/checkbox/index.mdx | 17 ++++++++++++++++- .../content/docs/components/checkbox/props.ts | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) 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 6e77de429..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. */