Skip to content

Add theming to Checkbox/Radio #2662

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jun 27, 2025
Merged

Add theming to Checkbox/Radio #2662

merged 31 commits into from
Jun 27, 2025

Conversation

marcysutton
Copy link
Member

Summary:

Adds theming support to Checkbox and Radio, including the Thunderblocks theme.

Notable differences in TB include:

  • Increased hover and target click areas (4px)
  • 4px border-radius instead of 3px
  • Semantic colors: Component/Input/Selected blue

Issue: WB-1972, WB-1974

Test plan:

  1. Review Test sheets for Checkbox and Radio
  2. Test clicking of padded wrapper around both component types
  3. Compare to Figma designs

Copy link

changeset-bot bot commented Jun 11, 2025

🦋 Changeset detected

Latest commit: fed0da3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 28 packages
Name Type
@khanacademy/wonder-blocks-form Minor
@khanacademy/wonder-blocks-tokens Patch
@khanacademy/wonder-blocks-dropdown Patch
@khanacademy/wonder-blocks-search-field Patch
@khanacademy/wonder-blocks-accordion Patch
@khanacademy/wonder-blocks-badge Patch
@khanacademy/wonder-blocks-banner Patch
@khanacademy/wonder-blocks-birthday-picker Patch
@khanacademy/wonder-blocks-breadcrumbs Patch
@khanacademy/wonder-blocks-button Patch
@khanacademy/wonder-blocks-cell Patch
@khanacademy/wonder-blocks-clickable Patch
@khanacademy/wonder-blocks-grid Patch
@khanacademy/wonder-blocks-icon-button Patch
@khanacademy/wonder-blocks-icon Patch
@khanacademy/wonder-blocks-labeled-field Patch
@khanacademy/wonder-blocks-layout Patch
@khanacademy/wonder-blocks-link Patch
@khanacademy/wonder-blocks-modal Patch
@khanacademy/wonder-blocks-pill Patch
@khanacademy/wonder-blocks-popover Patch
@khanacademy/wonder-blocks-progress-spinner Patch
@khanacademy/wonder-blocks-styles Patch
@khanacademy/wonder-blocks-switch Patch
@khanacademy/wonder-blocks-tabs Patch
@khanacademy/wonder-blocks-toolbar Patch
@khanacademy/wonder-blocks-tooltip Patch
@khanacademy/wonder-blocks-typography Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@khan-actions-bot khan-actions-bot requested a review from a team June 11, 2025 17:47
@khan-actions-bot
Copy link
Contributor

khan-actions-bot commented Jun 11, 2025

Gerald

Required Reviewers
  • @Khan/wonder-blocks for changes to pnpm-lock.yaml, .changeset/dull-experts-deny.md, .changeset/nice-toys-sparkle.md, __docs__/wonder-blocks-dropdown/multi-select.stories.tsx, __docs__/wonder-blocks-form/checkbox-accessibility.stories.tsx, __docs__/wonder-blocks-form/checkbox-group.stories.tsx, __docs__/wonder-blocks-form/checkbox.stories.tsx, __docs__/wonder-blocks-form/choice.stories.tsx, __docs__/wonder-blocks-form/radio-group.stories.tsx, __docs__/wonder-blocks-form/radio.stories.tsx, packages/wonder-blocks-dropdown/package.json, packages/wonder-blocks-form/package.json, packages/wonder-blocks-form/src/components/checkbox-core.tsx, packages/wonder-blocks-form/src/components/checkbox-group.tsx, packages/wonder-blocks-form/src/components/choice-internal.tsx, packages/wonder-blocks-form/src/components/radio-core.tsx, packages/wonder-blocks-form/src/components/radio-group.tsx, packages/wonder-blocks-form/src/theme/default.ts, packages/wonder-blocks-form/src/theme/index.ts, packages/wonder-blocks-form/src/theme/thunderblocks.ts, packages/wonder-blocks-form/src/util/styles.ts, packages/wonder-blocks-form/src/components/__tests__/checkbox.test.tsx, packages/wonder-blocks-tokens/src/theme/semantic/semantic-color-thunderblocks.ts

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

Copy link
Contributor

github-actions bot commented Jun 11, 2025

Size Change: +659 B (+0.65%)

Total Size: 101 kB

Filename Size Change
packages/wonder-blocks-form/dist/es/index.js 5.6 kB +659 B (+13.33%) ⚠️
ℹ️ View Unchanged
Filename Size
packages/wonder-blocks-accordion/dist/es/index.js 2.99 kB
packages/wonder-blocks-announcer/dist/es/index.js 1.74 kB
packages/wonder-blocks-badge/dist/es/index.js 1.75 kB
packages/wonder-blocks-banner/dist/es/index.js 1.91 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.91 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 723 B
packages/wonder-blocks-button/dist/es/index.js 4.12 kB
packages/wonder-blocks-cell/dist/es/index.js 2.06 kB
packages/wonder-blocks-clickable/dist/es/index.js 2.67 kB
packages/wonder-blocks-core/dist/es/index.js 2.48 kB
packages/wonder-blocks-data/dist/es/index.js 5.48 kB
packages/wonder-blocks-dropdown/dist/es/index.js 16.6 kB
packages/wonder-blocks-grid/dist/es/index.js 1.24 kB
packages/wonder-blocks-icon-button/dist/es/index.js 3.05 kB
packages/wonder-blocks-icon/dist/es/index.js 2.02 kB
packages/wonder-blocks-labeled-field/dist/es/index.js 2.92 kB
packages/wonder-blocks-layout/dist/es/index.js 1.63 kB
packages/wonder-blocks-link/dist/es/index.js 1.64 kB
packages/wonder-blocks-modal/dist/es/index.js 4.66 kB
packages/wonder-blocks-pill/dist/es/index.js 1.31 kB
packages/wonder-blocks-popover/dist/es/index.js 4.32 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.48 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.09 kB
packages/wonder-blocks-styles/dist/es/index.js 467 B
packages/wonder-blocks-switch/dist/es/index.js 1.55 kB
packages/wonder-blocks-tabs/dist/es/index.js 3.67 kB
packages/wonder-blocks-testing-core/dist/es/index.js 3.51 kB
packages/wonder-blocks-testing/dist/es/index.js 985 B
packages/wonder-blocks-theming/dist/es/index.js 577 B
packages/wonder-blocks-timing/dist/es/index.js 1.37 kB
packages/wonder-blocks-tokens/dist/es/index.js 4.84 kB
packages/wonder-blocks-toolbar/dist/es/index.js 900 B
packages/wonder-blocks-tooltip/dist/es/index.js 6.42 kB
packages/wonder-blocks-typography/dist/es/index.js 1.55 kB

compressed-size-action

Copy link
Contributor

github-actions bot commented Jun 11, 2025

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (1cedf6f) and published all packages with changesets to npm.

You can install the packages in frontend by running:

./dev/tools/deploy_wonder_blocks.js --tag="PR2662"

Packages can also be installed manually by running:

pnpm add @khanacademy/wonder-blocks-<package-name>@PR2662

Copy link
Contributor

github-actions bot commented Jun 11, 2025

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-hodwuxpjep.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 369
Tests with visual changes 8
Total stories 635
Inherited (not captured) snapshots [TurboSnap] 0
Tests on the build 369

Copy link
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

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

Nice work so far! Took a first pass at comparing the snapshots with the TB designs and left some comments in the PR and Chromatic. The TB radio button doesn't show up in the Chromatic diffs for some reason, so I didn't leave comments in Chromatic for that component (there are some visual differences though, like the disabled radio button colors!)

@@ -28,7 +28,8 @@
"@khanacademy/wonder-blocks-search-field": "workspace:*",
"@khanacademy/wonder-blocks-timing": "workspace:*",
"@khanacademy/wonder-blocks-tokens": "workspace:*",
"@khanacademy/wonder-blocks-typography": "workspace:*"
"@khanacademy/wonder-blocks-typography": "workspace:*",
"@khanacademy/wonder-blocks-form": "workspace:*"
Copy link
Member

Choose a reason for hiding this comment

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

Ohhh it makes sense that we need wonder-blocks-form in wonder-blocks-dropdown since we import the TextField component in the dropdown package. It is odd that it is only showing an error now since it was importing it before already 🤔

@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-form": minor
Copy link
Member

Choose a reason for hiding this comment

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

We can include the tokens package to the changeset too to be sure that it gets updated with the new css variables! (related #2660 (comment))

height: size / 2,
width: size / 2,
borderRadius: border.radius.radius_full,
top: `calc(${baseStyles.size} * .25 + ${theme.inputWrapper.padding})`,
Copy link
Member

Choose a reason for hiding this comment

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

Can we add a comment for why we multiply by .25 😄

Comment on lines 5 to 6
padding: "0px",
margin: "0px",
Copy link
Member

Choose a reason for hiding this comment

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

Could we use sizing.size_0 for spacing? Do we also want to group this into a layout group following the component token structure?

inputWrapper: {
  layout: {
    padding: sizing.size_0,
    margin: sizing.size_0,
  }
}

@khan-actions-bot khan-actions-bot requested a review from a team June 18, 2025 23:25
@beaesguerra
Copy link
Member

Thanks for making these updates! Left some comments in Chromatic!

Comment on lines 143 to 144
// Use the global focus style
...focusStyles.focus,
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: I think you'll need to target the :focus-visible pseudo-class here.

Suggested change
// Use the global focus style
...focusStyles.focus,
// Use the global focus style
...focusStyles.focus[":focus-visible],

Also this should work a bit better (no need to spread the object):

":focus-visible:not([disabled])": focusStyles.focus[":focus-visible]

Copy link
Member Author

Choose a reason for hiding this comment

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

Oddly it is working without focus-visible! Clicking a checkbox doesn't persist a focus style, while the focus style is visible with the keyboard.

Comment on lines 178 to 180
":focus-visible:not([disabled])": {
// Use the global focus style
...focusStyles.focus,
Copy link
Member

Choose a reason for hiding this comment

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

Same suggestion around :focus-visible here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah thanks, I missed one!

checkbox: {
border: {
radius: {
default: "3px",
Copy link
Member

Choose a reason for hiding this comment

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

question: What happens if you use a valid border_radius token here? would it change too much the layout?

Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to keep the Classic style as close as possible, and it had a 3px radius.

But now that you mention it, we went with the TB style for other characteristics. This was one of the few differences I kept between themes. TB has a 4px radius. Should we check with Design on that one or just change it? I don't think a border-radius will really affect the layout. It might not even be that noticeable.

Copy link
Member

Choose a reason for hiding this comment

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

I'd say that changing to 4px should be a safe change, but better double-checking with design as you suggest.

Comment on lines 87 to 93
disabled: {
// the semanticColor isn't working for this icon.
// our approach changed to use the TB theme in Classic,
// but the icon foreground color is themed and no longer works.
// semanticColor for TB: core.border.neutral.subtle (too dark in classic)
foreground: "#CBCBCD",
},
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: If this is for the icon, would core.foreground.neutral.subtle work instead? I wonder if this token is worth moving to the theme folder.

Copy link
Member Author

@marcysutton marcysutton Jun 24, 2025

Choose a reason for hiding this comment

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

I do think we need to use a different color, as the token in Figma is too dark on the background color.

Bea and I both thought this was too hard to see at the default size (TB screenshot):
Disabled checkbox screenshot with gray checkmark on gray

This is with core.foreground.neutral.subtle on Classic. I think the checkmark looks better:
Disabled checkbox screenshot with lighter gray checkmark

I committed that change, thank you for the suggestion!

Copy link
Member Author

Choose a reason for hiding this comment

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

And now that I see it, that Label text is suuuper light in Classic too. Disabled components are exempt from contrast requirements, but it still gets into the realm of usability. I think we need some consistency across disabled components and labels, particularly for text colors....but for text sizes too. Maybe we could do an overall polishing effort to make sure compound forms look good with components used together?

Copy link
Member

Choose a reason for hiding this comment

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

And now that I see it, that Label text is suuuper light in Classic too

Maybe the disabled label text color is something we can theme for these components? I find the current disabled text styling in Classic easier to read!
image

Maybe we could do an overall polishing effort to make sure compound forms look good with components used together

Agreed! The placement of the error message for CheckboxGroup and RadioGroup is different from how we place error messages under the field in LabeledField. Something we can make more consistent as well!

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is best suited for a follow-up issue on disabled colors as a whole. Here's why: the Classic token uses core.foreground.disabled.default, which has a different value per theme:

  • OG: core.foreground.disabled.default is default: color.fadedOffBlack32
  • TB: core.foreground.disabled.default is default: color.gray_50

Maybe I could hard-code a primitive value in the component theme? Or is there a better way?

Copy link
Member

Choose a reason for hiding this comment

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

I think we want to avoid using primitive values directly! One we way we can approach it is to have a component theme where different semantic tokens are used for OG and TB.

I'm doing something similar for the label in LabeledField:

disabled: {
foreground: semanticColor.core.foreground.neutral.strong,
},

disabled: {
foreground: semanticColor.core.foreground.disabled.subtle,
},

@khan-actions-bot khan-actions-bot requested a review from a team June 24, 2025 16:53
{(label || description || errorMessage) && (
<Strut size={spacing.small_12} />
)}
<View
Copy link
Member Author

Choose a reason for hiding this comment

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

We might want to go about this a different way... To remove Strut, I wrapped the affected elements in a View and put styling on it instead. But it broke the direct descendant behavior for fieldset > legend. I worked around this by explicitly adding an aria-labelledby attribute onto the fieldset, so it would have an accessible name exposed. But this might be a case where Strut is better suited for the problem.

Copy link
Member

Choose a reason for hiding this comment

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

what if you move the gap style to the fieldset element directly? that way, there's no need to define an intermediate wrapper so the fieldset > legend connection is not affected (FWIW that fieldset instance uses flex)

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem I ran into was fieldset potentially containing multiple elements: StyledLegend, description, and errorMessage. A gap would apply between all of these, rather than at the end. I wanted to group them and apply a margin to the container. I just pushed an update that wraps the children below in a View, and optionally puts marginBlockStart at the top of that instead!

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, that didn't work -- I couldn't keep the flex properties on the children and put a margin on it. I opted to restore Strut and kick the can down the road!

Copy link
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

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

Thanks for making all the UI adjustments, UI changes look good! I left a suggestion for the disabled label in case you wanted to include it in this PR or in a followup change!

Before approving, I wanted to get your thoughts on the theme structure for the form package since I'll be adding to it too for theming TextArea and TextField!

];

const wrapperStyle = [theme.inputWrapper, stateStyles.inputWrapper];
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about explicitly creating an aphrodite style for the inputWrapper css properties? In this case, theme.inputWrapper has valid CSS properties so it works out! But if we add other tokens to that structure it may not work. What comes to mind for example is if we add a foreground property to that object, it won't map to a css property!

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call. I added that!

Comment on lines 4 to 7
inputWrapper: {
padding: sizing.size_0,
margin: sizing.size_0,
},
Copy link
Member

Choose a reason for hiding this comment

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

Since this theme is for the form package, I am also adding to this structure for shared component theme tokens for TextField and TextArea! Since inputWrapper will only apply to radio/checkboxes, would it make sense to create a group for it? Something like:

export default {
  choice: { // <-- new choice group
    inputWrapper: {
      layout: { // <-- also new, to group the layout properties together
        padding: sizing.size_0,
        margin: sizing.size_0,
      }
    }
  },
  checkbox: { ... },
  field: { ... }, // <-- component theme tokens for TextField and TextArea
}

Let me know what you think!

Copy link
Member Author

Choose a reason for hiding this comment

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

That makes total sense! Thanks Bea!

@khan-actions-bot khan-actions-bot requested a review from a team June 26, 2025 21:27
Copy link
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

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

Looks good to me! 🚀

@marcysutton marcysutton merged commit a60d5ac into main Jun 27, 2025
14 checks passed
@marcysutton marcysutton deleted the WB-1972-checkradio branch June 27, 2025 16:40
Copy link

codecov bot commented Jun 27, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 0.00%. Comparing base (705ee01) to head (fed0da3).
Report is 12 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@     Coverage Diff      @@
##   main   #2662   +/-   ##
============================
============================

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 705ee01...fed0da3. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

marcysutton added a commit that referenced this pull request Jun 27, 2025
marcysutton added a commit that referenced this pull request Jun 27, 2025
## Summary:
Adds theming support to Checkbox and Radio, including the Thunderblocks theme.

Notable differences in TB include:
- Increased hover and target click areas (4px)
- 4px border-radius instead of 3px
- Semantic colors: Component/Input/Selected blue

Issue: WB-1972, WB-1974

## Test plan:
1. Review Test sheets for Checkbox and Radio
2. Test clicking of padded wrapper around both component types
3. Compare to Figma designs
   - TB: https://www.figma.com/design/EuFu0U7gqc1koXm8ZhlOLp/%E2%9A%A1%EF%B8%8F-Components?node-id=2048-52&t=B2xxLXv7GkDySMja-0
   - OG: https://www.figma.com/design/VbVu3h2BpBhH80niq101MHHE/%F0%9F%92%A0-Main-Components?node-id=15197-6798&t=zmIZ176DtJ5rmwyU-0

Author: marcysutton

Reviewers: beaesguerra, jandrade, marcysutton

Required Reviewers:

Approved By: beaesguerra

Checks: ✅ 12 checks were successful, ⏭️  2 checks have been skipped

Pull Request URL: #2662
marcysutton added a commit that referenced this pull request Jun 27, 2025
Adds theming support to Checkbox and Radio, including the Thunderblocks theme.

Notable differences in TB include:
- Increased hover and target click areas (4px)
- 4px border-radius instead of 3px
- Semantic colors: Component/Input/Selected blue

Issue: WB-1972, WB-1974

1. Review Test sheets for Checkbox and Radio
2. Test clicking of padded wrapper around both component types
3. Compare to Figma designs
   - TB: https://www.figma.com/design/EuFu0U7gqc1koXm8ZhlOLp/%E2%9A%A1%EF%B8%8F-Components?node-id=2048-52&t=B2xxLXv7GkDySMja-0
   - OG: https://www.figma.com/design/VbVu3h2BpBhH80niq101MHHE/%F0%9F%92%A0-Main-Components?node-id=15197-6798&t=zmIZ176DtJ5rmwyU-0

Author: marcysutton

Reviewers: beaesguerra, jandrade, marcysutton

Required Reviewers:

Approved By: beaesguerra

Checks: ✅ 12 checks were successful, ⏭️  2 checks have been skipped

Pull Request URL: #2662
marcysutton added a commit that referenced this pull request Jun 30, 2025
…2694)

## Summary:
This PR includes the following commits:
- Add theming to Checkbox/Radio (#2662)

Issue: WB-1972

## Test plan:

Author: marcysutton

Reviewers: beaesguerra

Required Reviewers:

Approved By: beaesguerra

Checks: ✅ 11 checks were successful, ⏭️  2 checks have been skipped, ⌛ 1 check is pending

Pull Request URL: #2694
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants