Skip to content

Conversation

@dmytrokirpa
Copy link
Contributor

@dmytrokirpa dmytrokirpa commented Nov 14, 2025

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.ts or .unstyled.ts style 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--docs

How to use it

  1. Configure a bundler's (Vite/Webpack/etc) resolution to prefer .headless.js files when they are available
  2. Instead of regular FluentProvider use ThemelessFluentProvider from contrib as we don't want to use Griffel providers.
  3. use Button, as it's the only headless component atm
import { ThemelessFluentProvider } from '@fluentui-contrib/react-themeless-provider';

<ThemelessFluentProvider>
  <Button appearance="primary">Button</Button>
<ThemelessFluentProvider>

You 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

Screenshot 2025-11-14 at 12 48 22
Headless/Unstyled
Screenshot 2025-11-14 at 12 50 20

@dmytrokirpa dmytrokirpa self-assigned this Nov 14, 2025
@dmytrokirpa dmytrokirpa changed the base branch from master to headless-components November 14, 2025 11:58
@github-actions
Copy link

Pull request demo site: URL

@dmytrokirpa dmytrokirpa changed the title Feat/headless button feat(react-button): introduce headless style hooks for button components Nov 14, 2025
@dmytrokirpa dmytrokirpa marked this pull request as ready for review November 14, 2025 12:29
@dmytrokirpa dmytrokirpa requested review from a team and marcosmoura as code owners November 14, 2025 12:29
// User provided class name
state.root.className,
]
.filter(Boolean)
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

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 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'];
Copy link
Contributor Author

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

@dmytrokirpa dmytrokirpa changed the base branch from headless-components to master November 14, 2025 12:47
// User provided class name
state.root.className,
]
.filter(Boolean)
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 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.

@github-actions
Copy link

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-breadcrumb
@fluentui/react-breadcrumb - package
114.136 kB
31.456 kB
92.397 kB
27.543 kB
-21.739 kB
-3.913 kB
react-button
Button
36.611 kB
10.409 kB
9.689 kB
3.863 kB
-26.922 kB
-6.546 kB
react-button
CompoundButton
43.012 kB
11.71 kB
10.566 kB
4.038 kB
-32.446 kB
-7.672 kB
react-button
MenuButton
41.551 kB
11.818 kB
16.431 kB
6.549 kB
-25.12 kB
-5.269 kB
react-button
SplitButton
49.613 kB
13.391 kB
19.343 kB
7.182 kB
-30.27 kB
-6.209 kB
react-button
ToggleButton
52.549 kB
12.196 kB
10.496 kB
4.151 kB
-42.053 kB
-8.045 kB
react-charts
AreaChart
401.301 kB
122.791 kB
377.014 kB
117.689 kB
-24.287 kB
-5.102 kB
react-charts
DeclarativeChart
718.606 kB
208.058 kB
694.302 kB
202.681 kB
-24.304 kB
-5.377 kB
react-charts
DonutChart
297.339 kB
88.898 kB
273.039 kB
83.379 kB
-24.3 kB
-5.519 kB
react-charts
FunnelChart
288.594 kB
85.727 kB
264.294 kB
80.515 kB
-24.3 kB
-5.212 kB
react-charts
GanttChart
383.058 kB
115.953 kB
358.758 kB
110.359 kB
-24.3 kB
-5.594 kB
react-charts
GaugeChart
312.216 kB
92.539 kB
287.929 kB
87.272 kB
-24.287 kB
-5.267 kB
react-charts
GroupedVerticalBarChart
390.846 kB
118.496 kB
366.559 kB
113.156 kB
-24.287 kB
-5.34 kB
react-charts
HeatMapChart
385.138 kB
117.661 kB
360.838 kB
112.428 kB
-24.3 kB
-5.233 kB
react-charts
HorizontalBarChart
294.96 kB
86.77 kB
270.673 kB
81.445 kB
-24.287 kB
-5.325 kB
react-charts
Legends
234.066 kB
69.154 kB
209.758 kB
63.896 kB
-24.308 kB
-5.258 kB
react-charts
LineChart
410.571 kB
124.202 kB
386.284 kB
119.136 kB
-24.287 kB
-5.066 kB
react-charts
ScatterChart
390.353 kB
118.433 kB
366.066 kB
113.126 kB
-24.287 kB
-5.307 kB
react-charts
VerticalBarChart
425.412 kB
123.541 kB
401.125 kB
118.395 kB
-24.287 kB
-5.146 kB
react-charts
VerticalStackedBarChart
397.198 kB
119.445 kB
372.898 kB
114.099 kB
-24.3 kB
-5.346 kB
react-components
react-components: Button, FluentProvider & webLightTheme
68.702 kB
19.799 kB
46.308 kB
15.119 kB
-22.394 kB
-4.68 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
235.705 kB
68.293 kB
213.956 kB
63.926 kB
-21.749 kB
-4.367 kB
react-components
react-components: entire library
1.278 MB
320.665 kB
1.229 MB
312.038 kB
-49.742 kB
-8.627 kB
Unchanged fixtures
Package & Exports Size (minified/GZIP)
global-context
createContext
510 B
328 B
global-context
createContextSelector
537 B
339 B
keyboard-keys
Multiple keyCodes
50 B
70 B
keyboard-keys
Multiple keys
87 B
94 B
keyboard-keys
Single key
44 B
64 B
keyboard-keys
Single keyCode
39 B
59 B
priority-overflow
createOverflowManager
4.457 kB
1.832 kB
react
ActivityItem
71.22 kB
23.347 kB
react
Announced
38.472 kB
13.275 kB
react
Autofill
15.42 kB
4.766 kB
react
Breadcrumb
200.805 kB
59.601 kB
react
Button
194.354 kB
55.886 kB
react
ButtonGrid
179.242 kB
53.891 kB
react
Calendar
121.162 kB
36.83 kB
react
Callout
84.299 kB
27.593 kB
react
Check
53.206 kB
17.835 kB
react
Checkbox
59.978 kB
19.874 kB
react
ChoiceGroup
65.488 kB
21.465 kB
react
ChoiceGroupOption
58.769 kB
19.353 kB
react
Coachmark
92.7 kB
29.305 kB
react
Color
7.789 kB
3.127 kB
react
ColorPicker
134.97 kB
42.125 kB
react
ComboBox
250.687 kB
71.515 kB
react
CommandBar
201.861 kB
59.387 kB
react
ContextualMenu
154.229 kB
47.566 kB
react
DatePicker
183.251 kB
55.892 kB
react
DateTimeUtilities
5.244 kB
1.849 kB
react
DetailsList
229.929 kB
65.81 kB
react
Dialog
210.16 kB
62.358 kB
react
Divider
19.588 kB
6.84 kB
react
DocumentCard
215.843 kB
63.666 kB
react
DragDrop
8.343 kB
2.724 kB
react
DraggableZone
34.28 kB
11.488 kB
react
Dropdown
233.151 kB
67.962 kB
react
ExtendedPicker
96.823 kB
27.866 kB
react
Fabric
41.728 kB
14.343 kB
react
Facepile
209.377 kB
62.375 kB
react
FloatingPicker
240.865 kB
68.222 kB
react
FocusTrapZone
16.99 kB
5.891 kB
react
FocusZone
55.1 kB
17.451 kB
react
Grid
179.242 kB
53.891 kB
react
GroupedList
135.035 kB
40.67 kB
react
GroupedListV2
122.659 kB
37.758 kB
react
HoverCard
96.784 kB
30.688 kB
react
Icon
51.887 kB
17.263 kB
react
Icons
66.339 kB
24.385 kB
react
Image
46.901 kB
15.695 kB
react
Keytip
81.301 kB
26.677 kB
react
KeytipData
14.05 kB
4.583 kB
react
KeytipLayer
103.089 kB
31.9 kB
react
Keytips
105.873 kB
32.904 kB
react
Label
38.324 kB
13.241 kB
react
Layer
48.089 kB
16.348 kB
react
Link
39.665 kB
13.653 kB
react
List
39.346 kB
12.454 kB
react
MarqueeSelection
74.49 kB
22.402 kB
react
MessageBar
189.388 kB
56.33 kB
react
Modal
93.738 kB
30.223 kB
react
Nav
186.825 kB
55.723 kB
react
OverflowSet
33.354 kB
11.282 kB
react
Overlay
40.885 kB
14.077 kB
react
Panel
200.327 kB
59.336 kB
react
Persona
114.591 kB
36.435 kB
react
PersonaCoin
114.591 kB
36.435 kB
react
PersonaPresence
58.076 kB
19.372 kB
react
Pickers
297.91 kB
82.996 kB
react
Pivot
187.734 kB
56.5 kB
react
Popup
12.312 kB
4.197 kB
react
Positioning
22.764 kB
7.683 kB
react
PositioningContainer
73.445 kB
23.685 kB
react
ProgressIndicator
39.477 kB
13.528 kB
react
Rating
82.086 kB
26.09 kB
react
Fluent UI React (entire library)
1.019 MB
283.184 kB
react
ResizeGroup
13.35 kB
4.379 kB
react
ResponsiveMode
8.13 kB
2.966 kB
react
ScrollablePane
55.541 kB
17.718 kB
react
SearchBox
187.63 kB
55.936 kB
react
SelectableOption
724 B
413 B
react
SelectedItemsList
231.35 kB
67.176 kB
react
Selection
42.418 kB
12.26 kB
react
Separator
35.365 kB
12.132 kB
react
Shimmer
49.249 kB
16.258 kB
react
ShimmeredDetailsList
240.71 kB
68.549 kB
react
Slider
57.627 kB
19.198 kB
react
SpinButton
191.297 kB
57.006 kB
react
Spinner
41.759 kB
14.468 kB
react
Stack
42.039 kB
14.389 kB
react
Sticky
32.577 kB
10.488 kB
react
Styling
46.033 kB
15.135 kB
react
SwatchColorPicker
189.637 kB
57.417 kB
react
TeachingBubble
204.648 kB
60.317 kB
react
Text
36.886 kB
12.806 kB
react
TextField
80.798 kB
25.308 kB
react
Theme
43.486 kB
14.168 kB
react
ThemeGenerator
12.384 kB
4.116 kB
react
TimePicker
240.515 kB
69.311 kB
react
Toggle
46.201 kB
15.957 kB
react
Tooltip
87.073 kB
28.151 kB
react
Utilities
82.938 kB
25.15 kB
react
Viewport
23.872 kB
7.642 kB
react
WeeklyDayPicker
101.348 kB
31.644 kB
react
WindowProvider
1.059 kB
541 B
react-accordion
Accordion (including children components)
107.26 kB
32.84 kB
react-aria
ARIA - Default
237 B
181 B
react-avatar
Avatar
48.452 kB
15.431 kB
react-avatar
AvatarGroup
18.243 kB
7.299 kB
react-avatar
AvatarGroupItem
61.704 kB
19.432 kB
react-badge
Badge
24.987 kB
8.153 kB
react-badge
CounterBadge
25.767 kB
8.426 kB
react-badge
PresenceBadge
24.868 kB
9.063 kB
react-calendar-compat
Calendar Compat
149.207 kB
39.833 kB
react-card
Card - All
105.924 kB
29.733 kB
react-card
Card
98.592 kB
27.856 kB
react-card
CardFooter
13.549 kB
5.411 kB
react-card
CardHeader
16.082 kB
6.279 kB
react-card
CardPreview
13.633 kB
5.547 kB
react-charting
AreaChart
299.078 kB
93.301 kB
react-charting
ChartHoverCard
37.196 kB
12.7 kB
react-charting
DeclarativeChart
647.186 kB
182.764 kB
react-charting
DonutChart
194.195 kB
60.841 kB
react-charting
GanttChart
278.567 kB
87.144 kB
react-charting
GaugeChart
194.712 kB
60.486 kB
react-charting
GroupedVerticalBarChart
290.196 kB
90.072 kB
react-charting
HeatMapChart
281.428 kB
87.826 kB
react-charting
HorizontalBarChart
127.238 kB
39.931 kB
react-charting
HorizontalBarChartWithAxis
288.265 kB
89.081 kB
react-charting
Legends
151.235 kB
46.305 kB
react-charting
LineChart
328.606 kB
100.243 kB
react-charting
MultiStackedBarChart
181.411 kB
55.129 kB
react-charting
PieChart
134.239 kB
42.282 kB
react-charting
SankeyChart
149.298 kB
46.659 kB
react-charting
ScatterChart
284.835 kB
89.47 kB
react-charting
Sparkline
87.616 kB
29.671 kB
react-charting
StackedBarChart
175.092 kB
52.716 kB
react-charting
TreeChart
84.809 kB
26.636 kB
react-charting
VerticalBarChart
300.598 kB
91.596 kB
react-charting
VerticalStackedBarChart
296.447 kB
91.135 kB
react-charts
HorizontalBarChartWithAxis
63 B
83 B
react-charts
SankeyChart
199.518 kB
61.371 kB
react-charts
Sparkline
91.253 kB
28.726 kB
react-checkbox
Checkbox
34.315 kB
11.706 kB
react-color-picker
ColorArea
48.331 kB
16.993 kB
react-color-picker
ColorPicker
16.962 kB
6.832 kB
react-color-picker
ColorSlider
40.505 kB
15.041 kB
react-combobox
Combobox (including child components)
105.977 kB
34.465 kB
react-combobox
Dropdown (including child components)
106.601 kB
34.39 kB
react-components
react-components: FluentProvider & webLightTheme
43.528 kB
14.148 kB
react-datepicker-compat
DatePicker Compat
225.807 kB
63.893 kB
react-dialog
Dialog (including children components)
102.738 kB
30.646 kB
react-divider
Divider
20.524 kB
7.559 kB
react-field
Field
22.595 kB
8.505 kB
react-image
Image
14.371 kB
5.815 kB
react-input
Input
27.039 kB
8.993 kB
react-jsx-runtime
Classic Pragma
1.101 kB
550 B
react-jsx-runtime
JSX Dev Runtime
2.8 kB
1.247 kB
react-jsx-runtime
JSX Runtime
3.154 kB
1.359 kB
react-label
Label
13.697 kB
5.54 kB
react-link
Link
16.926 kB
6.734 kB
react-list
List
87.903 kB
26.08 kB
react-list
ListItem
111.488 kB
32.937 kB
react-menu
Menu (including children components)
164.412 kB
49.793 kB
react-menu
Menu (including selectable components)
167.394 kB
50.381 kB
react-message-bar
MessageBar (all components)
24.057 kB
8.884 kB
react-motion
@fluentui/react-motion - createMotionComponent()
4.109 kB
1.806 kB
react-motion
@fluentui/react-motion - createPresenceComponent()
5.771 kB
2.396 kB
react-motion
@fluentui/react-motion - PresenceGroup
1.727 kB
823 B
react-overflow
hooks only
11.977 kB
4.574 kB
react-persona
Persona
55.343 kB
17.311 kB
react-popover
Popover
131.282 kB
40.84 kB
react-portal
Portal
15.55 kB
5.398 kB
react-portal-compat
PortalCompatProvider
8.386 kB
2.624 kB
react-positioning
usePositioning
28.865 kB
10.146 kB
react-positioning
useSafeZoneArea
12.445 kB
5 kB
react-progress
ProgressBar
16.493 kB
6.507 kB
react-provider
FluentProvider
23.704 kB
8.452 kB
react-radio
Radio
31.698 kB
9.911 kB
react-radio
RadioGroup
14.787 kB
5.992 kB
react-select
Select
26.878 kB
9.735 kB
react-slider
Slider
37.115 kB
12.365 kB
react-spinbutton
SpinButton
34.362 kB
11.35 kB
react-spinner
Spinner
24.294 kB
8.104 kB
react-swatch-picker
@fluentui/react-swatch-picker - package
105.063 kB
30.239 kB
react-switch
Switch
34.448 kB
10.922 kB
react-table
DataGrid
160.194 kB
45.261 kB
react-table
Table (Primitives only)
41.882 kB
13.483 kB
react-table
Table as DataGrid
131.409 kB
36.253 kB
react-table
Table (Selection only)
69.797 kB
19.616 kB
react-table
Table (Sort only)
68.44 kB
19.233 kB
react-tag-picker
@fluentui/react-tag-picker - package
187.327 kB
56.145 kB
react-tags
InteractionTag
14.459 kB
5.759 kB
react-tags
Tag
30.314 kB
9.694 kB
react-tags
TagGroup
83.004 kB
24.47 kB
react-teaching-popover
TeachingPopover
101.867 kB
30.488 kB
react-text
Text - Default
16.088 kB
6.258 kB
react-text
Text - Wrappers
19.248 kB
6.592 kB
react-textarea
Textarea
25.421 kB
9.255 kB
react-theme
Single theme token import
69 B
89 B
react-theme
Teams: all themes
36.081 kB
7.658 kB
react-theme
Teams: Light theme
20.025 kB
5.723 kB
react-timepicker-compat
TimePicker
108.943 kB
35.995 kB
react-toast
Toast (including Toaster)
103.22 kB
30.88 kB
react-tooltip
Tooltip
57.9 kB
20.014 kB
react-tree
FlatTree
148.292 kB
42.393 kB
react-tree
PersonaFlatTree
150.12 kB
42.772 kB
react-tree
PersonaTree
146.181 kB
41.588 kB
react-tree
Tree
144.361 kB
41.232 kB
react-utilities
SSRProvider
180 B
160 B
🤖 This report was generated against 6cbe729fbbde03cc9a7ecdc7329d81634fc9d08b

@@ -0,0 +1,7 @@
{

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.

@dmytrokirpa dmytrokirpa changed the base branch from master to headless-components November 17, 2025 18:04
@dmytrokirpa dmytrokirpa requested a review from a team as a code owner November 17, 2025 18:04
@dmytrokirpa dmytrokirpa merged commit 5f51555 into microsoft:headless-components Nov 17, 2025
12 checks passed
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