Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions apps/www/src/content/docs/components/checkbox/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export const playground = {
disabled: {
type: 'checkbox',
defaultValue: false
},
size: {
type: 'select',
options: ['large', 'small'],
defaultValue: 'large'
}
},
getCode
Expand Down Expand Up @@ -52,6 +57,20 @@ export const statesExamples = {
]
};

export const sizeExamples = {
type: 'code',
tabs: [
{
name: 'Large (default)',
code: `<Checkbox size="large" />`
},
{
name: 'Small',
code: `<Checkbox size="small" />`
}
]
};

export const groupDemo = {
type: 'code',
code: `
Expand All @@ -73,6 +92,25 @@ export const groupDemo = {
</Checkbox.Group>`
};

export const groupHorizontalDemo = {
type: 'code',
code: `
<Checkbox.Group defaultValue={["banana"]} orientation="horizontal">
<Flex gap="small" align="center">
<Checkbox name="apple" id="ch-apple" />
<label htmlFor="ch-apple">Apple</label>
</Flex>
<Flex gap="small" align="center">
<Checkbox name="banana" id="ch-banana" />
<label htmlFor="ch-banana">Banana</label>
</Flex>
<Flex gap="small" align="center">
<Checkbox name="cherry" id="ch-cherry" />
<label htmlFor="ch-cherry">Cherry</label>
</Flex>
</Checkbox.Group>`
};

export const groupDisabledDemo = {
type: 'code',
code: `
Expand Down
17 changes: 16 additions & 1 deletion apps/www/src/content/docs/components/checkbox/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

<Demo data={playground} />

Expand Down Expand Up @@ -55,12 +55,24 @@ The Checkbox component supports multiple states to represent different selection

<Demo data={statesExamples} />

### Size Variants

The Checkbox component comes in two sizes: `large` (default) and `small`.

<Demo data={sizeExamples} />

### Group

Use `Checkbox.Group` to coordinate multiple checkboxes with shared state.

<Demo data={groupDemo} />

### Horizontal Group

Use the `orientation` prop to lay out checkboxes in a horizontal row.

<Demo data={groupHorizontalDemo} />

### Disabled Group

Disable the entire group to prevent user interaction.
Expand All @@ -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
36 changes: 35 additions & 1 deletion apps/www/src/content/docs/components/checkbox/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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<HTMLSpanElement>,
state: { checked: boolean; indeterminate: boolean }
) => React.ReactNode;
Comment on lines +58 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@base-ui/react Checkbox.Indicator render prop signature state object

💡 Result:

The render prop signature for Checkbox.Indicator in @base-ui/react is: render?: ReactElement | ((props: HTMLProps, state: Checkbox.Indicator.State) => ReactElement) | undefined The state object (Checkbox.Indicator.State) includes at minimum: - transitionStatus: TransitionStatus From analogous components like Radio.Indicator.State, it likely also includes: - checked: boolean The official Base UI documentation shows this signature explicitly for the render prop, confirming it receives props and state parameters. The exact full shape of Checkbox.Indicator.State is partially documented as having transitionStatus, with checked inferred from patterns in related components (Radio, Checkbox.Root.State).

Citations:


🏁 Script executed:

# Try to find the Base UI checkbox types in node_modules if available
# or search for any type definitions in the codebase that reference Base UI's Indicator state
find . -type f -name "*.ts" -o -name "*.tsx" | head -20 | xargs grep -l "Checkbox.Indicator.State\|CheckboxPrimitive.Indicator" 2>/dev/null | head -5

Repository: raystack/apsara

Length of output: 41


🏁 Script executed:

# Search for actual render prop usage on checkbox indicators
rg "render\s*[=:].*\(props.*state" apps/www/src --type ts --type tsx -A 3

Repository: raystack/apsara

Length of output: 86


🏁 Script executed:

# Check the structure of the codebase first
ls -la apps/www/src/content/docs/components/checkbox/

# Search for render prop usage with correct rg syntax
rg "render.*\(.*state" apps/www/src -t ts -A 2

# Also search in the entire repo for any references to Indicator state
rg "Indicator.*State\|CheckboxIndicator" . -t ts -t js

Repository: raystack/apsara

Length of output: 658


🏁 Script executed:

# Check if Base UI types are in node_modules
find . -path ./node_modules/@base-ui -name "*checkbox*" -type f 2>/dev/null | head -10

# Or look for package.json to see the Base UI version
cat package.json 2>/dev/null | grep -A 2 "@base-ui"

Repository: raystack/apsara

Length of output: 41


🏁 Script executed:

# Find the actual checkbox component implementation
find apps/www/src -name "*.ts" -o -name "*.tsx" | xargs grep -l "CheckboxPrimitive\|Checkbox.Indicator\|render.*props.*state" 2>/dev/null

# Look at the demo.ts file to see how the component is used
cat -n apps/www/src/content/docs/components/checkbox/demo.ts

Repository: raystack/apsara

Length of output: 5324


🏁 Script executed:

# Check what version of Base UI is being used and look for type definitions
find . -name "package.json" -type f | head -3 | xargs grep -l "@base-ui" 2>/dev/null

Repository: raystack/apsara

Length of output: 43


🏁 Script executed:

# Show lines 38-55 of props.ts to see the render prop documentation
sed -n '38,55p' apps/www/src/content/docs/components/checkbox/props.ts

Repository: raystack/apsara

Length of output: 499


🏁 Script executed:

# Search for the actual Checkbox component implementation
find apps -name "*.ts" -o -name "*.tsx" | xargs grep -l "Checkbox.*Indicator\|render.*props.*state" 2>/dev/null | grep -v node_modules | head -5

Repository: raystack/apsara

Length of output: 113


🏁 Script executed:

# Look for any TypeScript type definitions for Checkbox
find apps -name "*.ts" -type f | xargs grep -l "interface.*Checkbox\|type.*Checkbox" 2>/dev/null | head -10

Repository: raystack/apsara

Length of output: 113


Update the render prop state type documentation to include transitionStatus.

The documented state signature { checked: boolean; indeterminate: boolean } is incomplete. Base UI's Checkbox.Indicator state also includes transitionStatus: TransitionStatus, which is not reflected in the current documentation. Update the JSDoc comment to accurately represent the full state shape that the render function receives.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/checkbox/props.ts` around lines 44 - 50,
Update the JSDoc for the render prop so it documents the full state shape passed
to the custom indicator renderer: include transitionStatus of type
TransitionStatus alongside checked and indeterminate. Specifically, modify the
comment on the render?: (props: React.HTMLAttributes<HTMLSpanElement>, state: {
checked: boolean; indeterminate: boolean }) => React.ReactNode to show state: {
checked: boolean; indeterminate: boolean; transitionStatus: TransitionStatus }
and mention it mirrors Checkbox.Indicator's state.


/** Additional CSS class name. */
className?: string;
}
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,44 @@ describe('Checkbox.Group', () => {
});
});

describe('Orientation', () => {
it('renders vertical by default', () => {
render(
<Checkbox.Group data-testid='group'>
<Checkbox name='apple' />
<Checkbox name='banana' />
</Checkbox.Group>
);
const group = screen.getByTestId('group');
expect(group).toHaveClass(styles.group);
expect(group).not.toHaveClass(styles['group-horizontal']);
});

it('renders vertical when orientation="vertical"', () => {
render(
<Checkbox.Group orientation='vertical' data-testid='group'>
<Checkbox name='apple' />
<Checkbox name='banana' />
</Checkbox.Group>
);
const group = screen.getByTestId('group');
expect(group).toHaveClass(styles.group);
expect(group).not.toHaveClass(styles['group-horizontal']);
});

it('renders horizontal when orientation="horizontal"', () => {
render(
<Checkbox.Group orientation='horizontal' data-testid='group'>
<Checkbox name='apple' />
<Checkbox name='banana' />
</Checkbox.Group>
);
const group = screen.getByTestId('group');
expect(group).toHaveClass(styles.group);
expect(group).toHaveClass(styles['group-horizontal']);
});
});

describe('Selection Behavior', () => {
it('works with defaultValue', () => {
render(
Expand Down
69 changes: 69 additions & 0 deletions packages/raystack/components/checkbox/__tests__/checkbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ describe('Checkbox', () => {
});
});

describe('Sizes', () => {
const sizes = ['small', 'large'] as const;
sizes.forEach(size => {
it(`renders ${size} size`, () => {
render(<Checkbox size={size} />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveClass(styles[size]);
});
});

it('renders large size by default', () => {
render(<Checkbox />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveClass(styles.large);
});
});

describe('Checked State', () => {
it('renders unchecked by default', () => {
render(<Checkbox />);
Expand Down Expand Up @@ -133,6 +150,52 @@ describe('Checkbox', () => {
});
});

describe('Custom Indicator (render prop)', () => {
it('renders custom indicator via render prop', () => {
render(
<Checkbox
checked
render={(props, state) => (
<span {...props} data-testid='custom-indicator'>
{state.checked ? 'checked' : 'unchecked'}
</span>
)}
/>
);
expect(screen.getByTestId('custom-indicator')).toHaveTextContent(
'checked'
);
});

it('receives indeterminate state in render prop', () => {
render(
<Checkbox
indeterminate
render={(props, state) => (
<span {...props} data-testid='custom-indicator'>
{state.indeterminate ? 'mixed' : 'clear'}
</span>
)}
/>
);
expect(screen.getByTestId('custom-indicator')).toHaveTextContent('mixed');
});

it('renders default check icon when no render prop provided', () => {
const { container } = render(<Checkbox checked />);
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(<Checkbox indeterminate />);
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();
Expand Down Expand Up @@ -232,5 +295,11 @@ describe('Checkbox', () => {
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('aria-invalid', 'true');
});

it('supports required prop', () => {
render(<Checkbox required />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('data-required');
});
});
});
Loading
Loading