-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat(react-button): introduce headless style hooks for button components #35491
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
feat(react-button): introduce headless style hooks for button components #35491
Conversation
|
Pull request demo site: URL |
| // User provided class name | ||
| state.root.className, | ||
| ] | ||
| .filter(Boolean) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not totally sure if we should just merge classes by hand, set up a shared utility, or maybe use mergeClasses from Griffel. Open to any ideas - what do you all think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I understand it, the main value of mergeClasses is that, for Griffel styles, it ensures the last style "wins" for any specificity conflicts.
Seems like this won't be useful for headless so we should use something else like clsx.
Would be great if we had a way for users to inject their own utility here but I also think something like clsx or classnames is small enough that it should be fine to just pick one and ship it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the main benefit of using clsx or classnames over just concatenating the strings together?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding another dependency could get a bit confusing, since people might not know when to use mergeClasses vs clsx/classnames. Same goes for creating our own classname utility. So, I'm also leaning toward just sticking with inline string concatenation for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the main benefit of clsx/classnames over concatenation is getting rid of undefined/nullish values, which don't affect the runtime but stay in the generated HTML, so they still affect the amount of bits the clients are pulling in.
| }), | ||
| bundler: webpackBundler(config => { | ||
| config.resolve ??= {}; | ||
| config.resolve.extensions = ['.headless.js', '.js', '.jsx', '.ts', '.tsx']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can revert this, used it for bundle size report generation
| // User provided class name | ||
| state.root.className, | ||
| ] | ||
| .filter(Boolean) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the main benefit of clsx/classnames over concatenation is getting rid of undefined/nullish values, which don't affect the runtime but stay in the generated HTML, so they still affect the amount of bits the clients are pulling in.
📊 Bundle size reportUnchanged fixtures
|
| @@ -0,0 +1,7 @@ | |||
| { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🕵🏾♀️ visual changes to review in the Visual Change Report
vr-tests-react-components/Charts-DonutChart 3 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Charts-DonutChart.Dynamic - Dark Mode.default.chromium.png | 12638 | Changed |
| vr-tests-react-components/Charts-DonutChart.Dynamic.default.chromium.png | 27057 | Changed |
| vr-tests-react-components/Charts-DonutChart.Dynamic - RTL.default.chromium.png | 30791 | Changed |
vr-tests-react-components/Positioning 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Positioning.Positioning end.chromium.png | 878 | Changed |
| vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png | 157 | Changed |
vr-tests-react-components/TagPicker 3 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/TagPicker.disabled - RTL.disabled input hover.chromium.png | 635 | Changed |
| vr-tests-react-components/TagPicker.disabled - High Contrast.chromium.png | 1319 | Changed |
| vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png | 677 | Changed |
vr-tests/Callout 6 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests/Callout.Bottom right edge - RTL.default.chromium.png | 1124 | Changed |
| vr-tests/Callout.Bottom left edge.default.chromium.png | 2195 | Changed |
| vr-tests/Callout.Gap space 25.default.chromium.png | 2195 | Changed |
| vr-tests/Callout.Left bottom edge.default.chromium.png | 3182 | Changed |
| vr-tests/Callout.Right bottom edge.default.chromium.png | 3095 | Changed |
| vr-tests/Callout.Top right edge.default.chromium.png | 1146 | Changed |
vr-tests/react-charting-GaugeChart 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests/react-charting-GaugeChart.Basic.default.chromium.png | 2 | Changed |
vr-tests/react-charting-LineChart 4 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests/react-charting-LineChart.Events - Dark Mode.default.chromium.png | 16 | Changed |
| vr-tests/react-charting-LineChart.Multiple - Dark Mode.default.chromium.png | 181 | Changed |
| vr-tests/react-charting-LineChart.Multiple - RTL.default.chromium.png | 200 | Changed |
| vr-tests/react-charting-LineChart.Multiple.default.chromium.png | 192 | Changed |
vr-tests/react-charting-MultiStackBarChart 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests/react-charting-MultiStackBarChart.Basic_Absolute - RTL.default.chromium.png | 343 | Changed |
| vr-tests/react-charting-MultiStackBarChart.Basic_Absolute.default.chromium.png | 359 | Changed |
vr-tests/react-charting-VerticalBarChart 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests/react-charting-VerticalBarChart.Basic - Secondary Y Axis.default.chromium.png | 3 | Changed |
There were 4 duplicate changes discarded. Check the build logs for more information.

Problem
Some of our partners have specific design and technical needs—their designs don't use the Fluent 2 design system, and technically, they prefer to avoid Griffel and fluent tokens.
Solution
TLDR
To address this, we're looking at offering headless or unstyled v9 components. These would provide our components without built-in styles, including only slot classes and state modifier classes (similar to BEM).
Details
The main idea is explained in #35464. We plan to create
.headless.tsor.unstyled.tsstyle hooks that add static classes to component slots based on their state. Consumers would need to adjust their bundler resolution to use these headless/unstyled files when they're available. This method lets us make the change without affecting current functionality. We're already using a similar approach for "raw/unprocessed" styles: https://storybooks.fluentui.dev/react/?path=/docs/concepts-developer-unprocessed-styles--docsHow to use it
.headless.jsfiles when they are availableFluentProvideruseThemelessFluentProviderfrom contrib as we don't want to use Griffel providers.Button, as it's the only headless component atmYou can now apply your own styles using custom style hooks supported in headless components, or use any CSS method you prefer to style headless components.
Bundle size comparison
It’s not really a fair comparison since we’re looking at styled versus unstyled components. But like the problem statement says, our partners aren’t interested in using our default styles or Griffel, so they don’t want to deal with the extra bundle or runtime costs for stuff they won’t actually use.
Default
Headless/Unstyled