Skip to content

feat(radio, checkbox, switch): add composable Legend sub-component for group components#423

Merged
mattrothenberg merged 5 commits intomainfrom
feat/composable-group-legend
Apr 20, 2026
Merged

feat(radio, checkbox, switch): add composable Legend sub-component for group components#423
mattrothenberg merged 5 commits intomainfrom
feat/composable-group-legend

Conversation

@mattrothenberg
Copy link
Copy Markdown
Collaborator

Summary

  • Adds Radio.Legend, Checkbox.Legend, and Switch.Legend sub-components that accept className for full styling control
  • Makes the legend string prop optional when using the sub-component instead
  • Solves the problem of redundant visible legends when a group is already labeled by a parent Field or heading (e.g. className="sr-only" to visually hide)

Before

When a RadioGroup (or CheckboxGroup/SwitchGroup) was nested inside a Field, the legend created an unavoidable visual duplicate of the label with no way to hide it without targeting internal implementation details.

After

// Simple case still works exactly as before
<Radio.Group legend="Paths" defaultValue="all">
  <Radio.Item label="Allow all paths" value="all" />
</Radio.Group>

// New: composable legend with full className control
<Radio.Group defaultValue="all">
  <Radio.Legend className="sr-only">Paths</Radio.Legend>
  <Radio.Item label="Allow all paths" value="all" />
</Radio.Group>

Changes

File Change
radio.tsx, checkbox.tsx, switch.tsx Added Legend sub-component, made legend prop optional
*/index.ts, src/index.ts Exported new components and types
RadioDemo.tsx Added RadioLegendSrOnlyDemo, RadioLegendCustomDemo
CheckboxDemo.tsx Added CheckboxLegendSrOnlyDemo, CheckboxLegendCustomDemo
SwitchDemo.tsx Added SwitchGroupDemo, SwitchLegendSrOnlyDemo, SwitchLegendCustomDemo
radio.mdx, checkbox.mdx, switch.mdx Added docs sections + API reference for Legend sub-components
  • Reviews
  • bonk has reviewed the change
  • automated review not possible because: new API surface needs human design review
  • Tests
  • Additional testing not necessary because: existing 730 tests pass, legend is backwards-compatible, and new sub-components are thin wrappers around Base UI's Fieldset.Legend

…r group components

Add Radio.Legend, Checkbox.Legend, and Switch.Legend sub-components that
accept className for full styling control (e.g. sr-only to visually hide).
Make legend string prop optional when using the sub-component instead.

This solves the problem of redundant visible legends when a group is
already labeled by a parent Field or heading.

Includes docs demos and API reference entries for all three components.
Switch.Group now matches Radio.Group and Checkbox.Group styling:
no border, no padding, no rounded corners. Also normalizes legend
from text-lg to text-base.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 16, 2026

npm i https://pkg.pr.new/@cloudflare/kumo@423

commit: cebcabc

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

Docs Preview

View docs preview

Commit: 3a58aee

@mattrothenberg
Copy link
Copy Markdown
Collaborator Author

/bonk

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

Visual Regression Report — 17 changed, 20 unchanged

17 screenshot(s) with visual changes:

Button / Icon Only

32 px (0.03%) changed

Before After Diff
Before After Diff

Button / Loading State

3 px (0%) changed

Before After Diff
Before After Diff

Button / Title

422 px (0.42%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Actions

191 px (0.19%) changed

Before After Diff
Before After Diff

Dialog / Dialog Confirmation

352 px (0.35%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Select

244 px (0.24%) changed

Before After Diff
Before After Diff

Dialog (Open)

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Basic

299 px (0.29%) changed

Before After Diff
Before After Diff

Select / Select Sizes

170 px (0.09%) changed

Before After Diff
Before After Diff

Select / Select Without Label

183 px (0.18%) changed

Before After Diff
Before After Diff

Select / Select Placeholder

706 px (0.7%) changed

Before After Diff
Before After Diff

Select / Select Loading

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Multiple

556 px (0.55%) changed

Before After Diff
Before After Diff

Select / Select Complex

1,093 px (0.92%) changed

Before After Diff
Before After Diff

Select / Select Disabled Items

318 px (0.31%) changed

Before After Diff
Before After Diff

Select / Select Grouped With Disabled

450 px (0.44%) changed

Before After Diff
Before After Diff

Select (Open)

1,370 px (0.01%) changed

Before After Diff
Before After Diff
20 screenshot(s) unchanged
  • Button / Basic
  • Button / Variant: Primary
  • Button / Variant: Secondary
  • Button / Variant: Ghost
  • Button / Variant: Destructive
  • Button / Variant: Outline
  • Button / Variant: Secondary Destructive
  • Button / Sizes
  • Button / With Icon
  • Button / Disabled State
  • Dialog / Dialog Basic
  • Dialog / Dialog Alert
  • Dialog / Dialog With Combobox
  • Dialog / Dialog With Dropdown
  • Select / Select With Field
  • Select / Select With Tooltip
  • Select / Select Custom Rendering
  • Select / Select Disabled Options
  • Select / Select Grouped
  • Select / Select Long List

Generated by Kumo Visual Regression

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 16, 2026

Based on my thorough review of PR #423, here's my analysis:

Review Summary

This PR adds composable Legend sub-components (Radio.Legend, Checkbox.Legend, Switch.Legend) that provide full styling control via className. This solves a real accessibility issue where redundant visible legends appear when a group is already labeled by a parent Field or heading.

✅ What's Well Done

  1. Clean Implementation: Each Legend component is a thin, focused wrapper around Base UI's Fieldset.Legend with proper styling using semantic tokens (text-kumo-default, etc.)

  2. Consistent API: All three components follow the same pattern:

    • Made legend prop optional in the Group component
    • Added composable Legend sub-component with children and className props
    • Properly exported via compound component pattern and direct exports
  3. Follows Conventions:

    • ✅ Uses cn() for className composition
    • ✅ No raw Tailwind colors (semantic tokens only)
    • ✅ No dark: variants
    • ✅ All components have displayName set
    • ✅ Proper TypeScript types exported
  4. Comprehensive Documentation:

    • Demos added showing both sr-only and custom styling use cases
    • MDX docs updated with examples and API reference
    • Changeset properly created
  5. Backwards Compatible: The legend string prop still works exactly as before

🔍 Minor Observations

  1. Switch.Legend styling: Uses text-lg while Radio/Checkbox use text-base. This matches the existing Switch.Group legend styling, so it's consistent within Switch, but creates inconsistency between components. This appears intentional based on existing Switch.Group styling.

  2. Missing Switch.Group value props: Unlike Radio.Group and Checkbox.Group, Switch.Group doesn't have value/onValueChange props. This is pre-existing (not introduced by this PR), but worth noting.

🧪 Test Results

  • Radio tests pass ✅
  • No existing Checkbox or Switch tests (not a regression)
  • Lint/type failures are due to missing auto-generated files (ai/component-registry.json, ai/schemas.ts) - not related to PR changes

💡 Optional Enhancement

Consider adding a development warning if neither legend prop nor Legend sub-component is provided to ensure groups remain accessible.


Overall: This is a well-designed, well-implemented feature that follows all project conventions. The API is intuitive and solves a real problem. I recommend approving this PR.

github run

…component

RadioLegend, CheckboxLegend, SwitchLegend are only accessible as
Radio.Legend, Checkbox.Legend, Switch.Legend — no standalone named
exports. Type exports (RadioLegendProps, etc.) are still available.
@mattrothenberg
Copy link
Copy Markdown
Collaborator Author

/bonk can you please approve?

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 16, 2026

Based on my thorough review of PR #423, here's my analysis:

Review Summary

This PR adds composable Legend sub-components (Radio.Legend, Checkbox.Legend, Switch.Legend) that provide full styling control via className. This solves a real accessibility issue where redundant visible legends appear when a group is already labeled by a parent Field or heading.

✅ What's Well Done

  1. Clean Implementation: Each Legend component is a thin, focused wrapper around Base UI's Fieldset.Legend with proper styling using semantic tokens (text-kumo-default, etc.)

  2. Consistent API: All three components follow the same pattern:

    • Made legend prop optional in the Group component
    • Added composable Legend sub-component with children and className props
    • Properly exported via compound component pattern and direct exports
  3. Follows Conventions:

    • ✅ Uses cn() for className composition
    • ✅ No raw Tailwind colors (semantic tokens only)
    • ✅ No dark: variants
    • ✅ All components have displayName set
    • ✅ Proper TypeScript types exported
  4. Comprehensive Documentation:

    • Demos added showing both sr-only and custom styling use cases
    • MDX docs updated with examples and API reference
    • Changeset properly created
  5. Backwards Compatible: The legend string prop still works exactly as before

🔍 Minor Observations

  1. Switch.Legend styling: Uses text-base while Radio/Checkbox use text-base. This matches the existing Switch.Group legend styling, so it's consistent within Switch, but creates inconsistency between components. This appears intentional based on existing Switch.Group styling.

  2. Missing Switch.Group value props: Unlike Radio.Group and Checkbox.Group, Switch.Group doesn't have value/onValueChange props. This is pre-existing (not introduced by this PR), but worth noting.

🧪 Test Results

  • Radio tests pass ✅
  • No existing Checkbox or Switch tests (not a regression)
  • Lint/type failures are due to missing auto-generated files (ai/component-registry.json, ai/schemas.ts) - not related to PR changes

💡 Optional Enhancement

Consider adding a development warning if neither legend prop nor Legend sub-component is provided to ensure groups remain accessible.


Overall: This is a well-designed, well-implemented feature that follows all project conventions. The API is intuitive and solves a real problem. I recommend approving this PR.

github run

@mattrothenberg mattrothenberg merged commit 1954aa8 into main Apr 20, 2026
17 checks passed
@mattrothenberg mattrothenberg deleted the feat/composable-group-legend branch April 20, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants