-
Notifications
You must be signed in to change notification settings - Fork 2.9k
RFC: Unstyled Components #35464
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
RFC: Unstyled Components #35464
Conversation
|
Pull request demo site: URL |
f81e722 to
f2b8caf
Compare
f2b8caf to
2f51180
Compare
📊 Bundle size reportUnchanged fixtures
|
| - ✅ Component files unchanged (still supports `useCustomStyleHook_unstable`) | ||
| - ✅ **~25% JS bundle size reduction** (tested) by excluding Griffel runtime | ||
|
|
||
| **Note:** To completely eliminate Griffel from an application, unstyled variants are needed for **all components that use Griffel**, including infrastructure components like `FluentProvider`. This ensures no Griffel runtime is bundled. |
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 we already have this: https://github.com/microsoft/fluentui-contrib/tree/main/packages/react-themeless-provider
docs/react-v9/contributing/rfcs/react-components/convergence/unstyled-components.md
Outdated
Show resolved
Hide resolved
|
|
||
| Unstyled variants are opt-in via bundler extension resolution (similar to [raw modules](https://storybooks.fluentui.dev/react/?path=/docs/concepts-developer-unprocessed-styles--docs#how-to-use-raw-modules), ensuring zero breaking changes. | ||
|
|
||
| **Performance Impact:** Internal testing shows **~25% JavaScript bundle size reduction** when using unstyled variants, as Griffel runtime and style implementations are excluded from the bundle. |
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 will be an increase once you will have actual CSS that matches what we have currently? Nobody is going to use components without any styles.
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.
It mainly depends on how much styles consumers want to use. Even if the number (25%) isn't completely accurate, users will still need to pay for any default styles they don't intend to use.
I tried to check the increase with the actual CSS version this by switching the style hook to CSS modules, but our tooling (monosize) only takes into account JS, not CSS. It's possible I missed a configuration or setting.
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 tried to check the increase with the actual CSS version this by switching the style hook to CSS modules, but our tooling (monosize) only takes into account JS, not CSS. It's possible I missed a configuration or setting.
It indeed won't measure CSS by default.
IMO it's crucial to provide measurements to compare apples to apples. As currently, "25% reduction" is a false message.
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.
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 got what you mean, however you won't use solely JavaScript, correct? 🐱 Otherwise cards would look like that:
P.S. In the Griffel scenario we will need have part of Griffel runtime + mappings for merging which contributes to JS size.
I've ran "Griffel + AOT + CSS extraction" scenario for Card.fixture.js:
- JS 82.432 kB / 25.161 kB
- CSS 13K / 2.52 kB
- Total: 95.4 K / 27.6 K
Can you please provide the same for "Without Griffel - "unstyled" + plain CSS"? (considering that CSS file for Card should match current styles)
From the RFC, I noticed that the initial plan is to update 10 components, so it would good to have the same for all them to be realistic.
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.
we should probably provide a realistic sample implementation with different styles and use that for comparison. and make sure we measure 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.
we should probably provide a realistic sample implementation with different styles and use that for comparison. and make sure we measure it
Totally agree! Just to clarify, reducing bundle size wasn’t our only reason for this. The main goal is to support partners who have their own unique UI needs - like, their designs don’t use Fluent 2, they have specific tech/performance requirements, and they aren’t using Griffel or default Fluent styles. They’d rather not include stuff they don’t actually need.
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 asked this in a Teams thread but putting it here as well. I'd be curious what runtime perf numbers look like after this change as well. Given JS is more expensive client side than something like static CSS. I know Griffel supports extraction so we should compare that too, but it would be good to have a full perf profile. Not sure if anything has been setup with tensile yet but that might be helpful in testing this kind of thing.
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 asked this in a Teams thread but putting it here as well. I'd be curious what runtime perf numbers look like after this change as well. Given JS is more expensive client side than something like static CSS. I know Griffel supports extraction so we should compare that too, but it would be good to have a full perf profile. Not sure if anything has been setup with tensile yet but that might be helpful in testing this kind of thing.
That’s a good point! After giving it some thought, I feel like it’s probably outside the RFC’s scope. The main idea here isn’t to compare or change Fluent’s styling approach, but more about letting consumers fully provide their own styles instead of just overriding what’s already there.
| **Pros:** Single source of truth, automatic | ||
| **Cons:** Complex build config, harder to debug | ||
|
|
||
| ## Usage Examples |
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 you pls also include examples for styles applied conditionally based on state and/or props? for example how to style a toggle button with different background based on toggle state or how to style a secondary button?
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.
here is an example for Button component: https://github.com/microsoft/fluentui/pull/35491/files#diff-87994f1431cce0df0f1b30e5980466aae99f5bab0c73f40d39266af17e977caa
I'll add it to the doc
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.
updated
|
Thank you for this RFC. I would like to share some thoughts about this proposal. For Who is This RFC?
This RFC appears to be designed for teams who want to restyle Fluent UI components without overriding the default styles. This is a valuable use case. However, I would like to point out an important limitation of the current proposal. The component API remains unchanged, which means all design-related properties (such as This is particularly important because the RFC describes these components as "headless" or "styleless", but they still have design opinions built into their API 🚨 import { Button } from "@fluentui/react-components"
function App() {
return <Button appearance="primary" />
// ^ ⚠️ still valid for "headless" components
}For comparison, libraries like Base UI (which are truly unstyled) do not include design terms in their component API i.e. don't provide opinions on design. They only expose component states (such as I would like to better understand which specific problem this RFC aims to solve. If the goal is to provide truly unstyled components, would it make sense to also remove design-related terms from the component API? This question is important because it affects how flexible the solution can be for different use cases. |
Bundler magic concerns
Bundler configuration should be avoided when possibleThe another RFC mentions (RFC: Stop pre-processing styles with Griffel in I am surprised that this RFC proposes it as the primary approach. In general, it is better to avoid requiring users to modify their bundler configuration as it's a default solution in JS ecosystem 👆 1. Some tools do not allow configuration changesFor example, Create React App did not allow Webpack configuration changes without ejecting or using third-party tools like 2. Not all bundlers support this featureThe RFC shows examples for Vite and Webpack. However, there are many other bundlers in use today (Parcel, esbuild, Rspack, Rolldown, Turbopack, Rollup, etc.) including legacy solutions. We would need to verify that each bundler supports extension resolution in this way. We would also have to track support in future bundlers 💥
3. Other tools in the build chain need configuration tooEven if all bundlers support this feature, other tools in the development chain also need to be configured. Test runners need the same configuration:
Without proper configuration, tests could see different class names than the browser 🚨 For example:
4. Cannot use both styled and unstyled components togetherThe bundler configuration approach has a critical limitation: it is impossible to opt-out at the module level. Because the configuration is global for the entire application including third party dependencies. It might be considered as a benefit, but it's not (see next replies). With this approach you cannot use both styled and unstyled components from For example, if a team wants to:
This is not possible with the bundler configuration approach. The configuration applies to all bundle. This limitation means teams cannot gradually migrate to unstyled components. They must either:
There is no middle ground 💣 RecommendationWe should provide a solution that works without requiring bundler configuration changes. The solution should:
This approach would be easier for teams to adopt and more future-proof. |
Concerns about expected usageI have concerns about how the proposed solution works in practice. The RFC does not fully explore the developer experience, and I believe the actual usage conflicts with some of the stated goals in it. How will developers use unstyled components?Let me walk through a typical scenario to illustrate the challenges. Step 1: Initial setup A team decides to use unstyled components from Fluent UI. They:
Step 2: Adding styles The team wants to use Example style file: /* Button.css */
.fui-Button {
background-color: blue;
color: white;
/* other styles */
}They then import these styles in their app: // @filename packages/app/src/App.tsx
import { Button, Card, Divider } from '@fluentui/react-components';
import './Button.css';
import './Card.css';
import './Divider.css';
function App() {
return (
<Card>
<Button>Click me</Button>
<Divider />
</Card>
);
}This works correctly ✅ However, this is just the beginning. Step 3: Creating custom components The team extracts logic into a custom component: // @filename packages/app/src/MyCard.tsx
import { Card, Button, Divider } from '@fluentui/react-components';
import './Button.css';
import './Divider.css';
import './Card.css';
// ⬆️ they have to import CSS file in the component, too
export function MyCard() {
return (
<Card>
<Button>Click me</Button>
<Divider />
</Card>
);
}Now, every file that uses Fluent UI components must manually import the corresponding CSS files. This quickly becomes tedious and error-prone in large codebases with many developers. Problem 1: Manual style imports are error-proneDevelopers can easily forget to import styles: // @filename packages/app/src/MyButton.tsx
import { Button } from '@fluentui/react-components';
// Forgot to import './Button.css' ❌
export function MyButton() {
return <Button>Click me</Button>;
}This is especially problematic because the error may not be immediately visible. If another component has already imported the styles, the component will work 💥 // @filename packages/app/src/App.tsx
import { MyCard } from './MyCard';
import { MyButton } from './MyButton';
function App() {
return (
<>
<MyCard />
<MyButton /> {/* 🫲 Works because MyCard imported Button.css */}
</>
);
}However, if components are lazy loaded, there will be a flash of unstyled content (FOUC): // @filename packages/app/src/App.tsx
import { Suspense, lazy } from 'react';
import { MyButton } from './MyButton';
const MyCard = lazy(() => import('./MyCard'));
function App() {
return (
<>
<Suspense fallback={<div>Loading...</div>}>
<MyCard />
</Suspense>
<MyButton /> {/* 💥 FOUC - styles not loaded yet */}
</>
);
}Linting rules could help enforce CSS files imports, but this is not a good developer experience 🚨 Problem 2: First-party and third-party componentsWhat happens when first-party or third-party packages use // @filename node_modules/first-party-package/src/SomeComponent.tsx
import { Button } from '@fluentui/react-components';
export function SomeComponent() {
return <Button>Click me</Button>;
}This component will have no styles unless the application imports To be safe, developers might load all styles at the app entry point: // @filename packages/app/src/App.tsx
import { SomeComponent } from 'first-party-package';
import { MyCard } from './MyCard';
import './Button.css';
import './Card.css';
import './Divider.css';
// ... import all possible component styles `Menu`, `Popover`, `Carousel`, much more ⚠️
function App() {
return (
<>
<MyCard />
<SomeComponent />
</>
);
}However, this defeats the "pay for what you use" principle. The app now loads styles for components that may not be used at all or not needed for the initial load. Problem 3: Teams will likely recompose the design systemTo avoid manual style imports, teams will likely create wrapper components: // @filename packages/my-design-system/src/Button.tsx
import './Button.css';
export { Button } from '@fluentui/react-components';Then use these wrappers in the app: // @filename packages/app/src/App.tsx
import { Button } from 'my-design-system';
function App() {
return <Button>Click me</Button>;
}At this point, the team is maintaining their own design system that re-exports Fluent UI components. This is essentially the same as recomposing components, which is one of the problems the RFC aims to solve.
Conflicts with RFC goalsThese usage patterns conflict with the stated goals of the RFC: Goal: "You are no longer forced to override or fight default styles—simply provide your own styling from scratch." Reality: While technically true, developers must manually import styles in every file that uses components. This is not simple and becomes a maintenance burden. Or create their own design system... Goal: "You only pay for what you use: if you don't need the default Fluent styles, they are not included in your bundle at all." Reality: To avoid FOUC issues with third-party packages, developers may need to import all component styles at the app entry point, defeating the "pay as you go" principle.
Goal: "This approach enables a clean separation of behavior/accessibility from visual design, making Fluent UI a better foundation for custom design systems." Reality: Teams will likely need to recompose the design system anyway to avoid manual style imports everywhere. This recomposition work is what the RFC aims to eliminate. RecommendationThe RFC should include a detailed section on the expected developer workflow and how styles should be managed. This section should address:
Without clear guidance on these points, teams may find the solution difficult to adopt successfully. |
Compatibility concerns with Fluent UI ecosystemI have concerns about how headless components will work with the existing Fluent UI ecosystem. The RFC does not fully address compatibility issues that may arise. 1. FluentProvider requires GriffelThe RFC mentions using
2. Third-party packages likely use GriffelWhen first-party or third-party packages depend on // node_modules/first-party-package/src/SomeComponent.tsx
import { Button, makeStyles } from '@fluentui/react-components';
const useStyles = makeStyles({
customButton: {
/* custom styles */
},
});
export function SomeComponent() {
const styles = useStyles();
return <Button className={styles.customButton}>Click me</Button>;
}How will this work with headless components? The RFC does not explain this scenario. Some concerns:
3. Custom style hooks may not work as expectedCustom style hooks present another compatibility issue. The RFC keeps the same API and hook names (e.g., However, in reality, the behavior will be different. Custom style hooks do not create a strict contract on styles inside them, which means:
For example, Recommendation: Rename custom style hooksI suggest renaming the custom style hooks to make it clear that they work differently: export const Button = React.forwardRef((props, ref) => {
const state = useButton_unstable(props, ref);
- useCustomStyleHook_unstable('useButtonStyles_unstable')(state);
+ useCustomStyleHook_unstable('useBaseButtonStyles_unstable')(state);
return renderButton_unstable(state);
});This change would:
4. Design tokens and CSS variables are unclearThe RFC mentions that tokens are removed along with Griffel: "By omitting default styles and their dependencies (like Griffel and tokens), bundle size is reduced." However, the usage examples show usage of design tokens:
The RFC should clarify:
Caution This point is also linked to point 2 about third party components. If 4. Zero breaking changes claim needs clarificationThe RFC states: "Headless variants are opt-in via bundler extension resolution... ensuring zero breaking changes." This claim is technically correct but misleading:
The RFC should be more transparent about the breaking changes teams will face when adopting headless components. |
layershifter
left a comment
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.
-- comments on the PR --
1587eda to
2862d96
Compare
2862d96 to
5266418
Compare
|
|
||
| ## Bundle Size & CSS Measurements | ||
|
|
||
| Internal testing shows **~25% JavaScript bundle size reduction** for `Button` and `Divider` components by removing Griffel runtime and default style implementations when using unstyled variants. |
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.
is this metric comparison with same functionality in mind to compare apples to apples ? based on the "Results" chapter it is not clear enough to be validated.
eg:
- what are metrics for real application that needs to incorporate similar size of design tokens like current fluent
- what's the delta of AOT + griffel runtime vs vanilla CSS with same logic
- what's the delta of not including default icons ? is this metric based on new atomic icons API ?
Could we provide a simple app to get the outcomes first hand ?
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.
is this approach feasible for all our controls, especially more complex ones like react-card, or table which have a lot of "design/behaviour logic" ?
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.
We could come up with both data and and the app, but this is not a goal here as it goes a bit beyond the RFC. The goal is to provide a low-level building blocks that have no default styles/design opinions baked in. They could be styled with Griffel and existing tokens if needed. For cases when you need something custom, and don't want to pay for default implementation you don't plan to use.
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.
Mathematics
Please compare apples to apples or keep a neutral tone regarding measurements and provide a better explainer.
25% sounds BOLD and cool. BUT.
I've run the fixtures from this RFC:
| Fixture | Minified | GZIP |
|---|---|---|
| Button | 19.026 kB | 7.465 kB |
| ButtonUnstyled | 8.763 kB | 3.545 kB |
ButtonUnstyledas is,Buttonis with CSS extraction
So pure runtime addition is 3.92 kB (Griffel runtime + styling logic).
Given
- Connection speed: 400 Kbps Slow 3G
- File size: 3.92 kB
1. Convert kilobytes to kilobits:
Data Size = 3.92 KB × 8 bits/byte = 31.36 Kb
2. Calculate time:
Time = 31.36 Kb ÷ 400 Kbps = 0.0784 seconds
Result
0.0784 seconds (approximately 78.4 milliseconds)
Otherwise, it's fair to share that we will load 78ms faster on slow 3G.
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.
No one said that the 78ms improvement on slow 3G was wrong, so no worries there. I’ll add a note to make it clear these numbers only apply if you’re not using or planning to use default styles - it’s not about Griffel vs CSS. I’m purposely avoiding a direct apples-to-apples comparison since we all know how that would turn out 😄. The numbers are for cases where you override the styles but still have to deal with the cost. Hope that clears up any questions or confusion about the stats!
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.
@layershifter I have some validation data from an actual product and can walk you through it if you're interested. I think the tone here is appropriate, as it accurately reflects the situation.
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 tone here is appropriate, as it accurately reflects the situation.
No, it's not. It still delivers the wrong message - "switch to unstyled components to reduce bundle size" (i.e. I have the same concerns as @Hotell).
we typically see more modest JS savings around ~25% due to other application code and dependencies
Do I correctly understand that once we will migrate Teams/Word/any other product to unstyled components, we will see ~25% bundle size reduction? I know the answer in advance, if it will be 0.001% improvement, we will be lucky.
Proper messaging:
- The design system part of Fluent UI has cost (tokens, design terms, built-in styles)
- Switch to unstyled components compared to default ones will reduce bundle size, but your custom design system will also have cost (if your goal to replace Fluent)
- If you are building a reasonably small app (<1MB of JS, with a limited usage of any design system), you may see reasonable bundle size cuts
- If you are going to move a large product to replace Fluent with unstyled components + custom design system (>10MB of JS), you will likely see no reasonable reductions
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.
No, it's not. It still delivers the wrong message - "switch to unstyled components to reduce bundle size" (i.e. I have the same concerns as @Hotell).
I think there’s a key point missing here:
the numbers are only applicable for cases where you override the styles but still have to pay their cost of default styles/tokens/icons you don't actually use.
Do I correctly understand that once we will migrate Teams/Word/any other product to unstyled components, we will see ~25% bundle size reduction? I know the answer in advance, if it will be 0.001% improvement, we will be lucky.
Just to clear things up - the 25% reduction only applies to the FluentUI part, which is just a small piece of the whole app. Cutting down FluentUI won’t have such a big impact on the overall bundle size of a huge app. I can call that out more directly if it's not obvious - we can’t shrink parts we don’t control. What we can do is give users a lighter option for custom scenarios when they need it.
Switch to unstyled components compared to default ones will reduce bundle size, but your custom design system will also have cost (if your goal to replace Fluent)
That’s totally true, and I’m not saying otherwise. The reduction comes from cutting out stuff you’re not using, since you want to add your own styles. Honestly, I’m not sure how else to highlight this point!
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.
The focus and goal of this initiative is to provide bare bones top grade fluent a11y and common UX patterns without DS styles for complete DYI functionality and easier gen AI enablement, that were created by following industry best practices via https://open-ui.org/ across multiple iterations.
We should focus on the actual implementation and API approach which is almost in the ballpark already with this PR, rather than focusing / highlighting performance impact that may be misleading for real products.
Perfomance
Regarding the perf data it would be great to actually understand the griffel runtime with AOT in place vs raw CSS in apples to apples to comparison.
- what delta is the actual griffel runtime
- what delta is custom logic for applying extracted css classnames vs raw CSS etc
We have separate perf and bundle size initiatives in progress that should be finished first before making official statements that Unstyled components will get you 25% bundle size reduction without knowledge of additional costs involved ( maintenance, custom code, a11y etc ).
to mention some of the perf initiatives:
- atomic react icons use ( 20% perf improvements in some real apps )
- avoiding default slots ( icons mainly )
- tree shakeable tokens
- modern css usage within griffel styles ( every --webkit-* variant etc adds to the final bundle )
- native browser features usage vs artificial ones ( positioning api etc )
- ability to opt out from current / replace with custom
docs/react-v9/contributing/rfcs/react-components/convergence/unstyled-components.md
Show resolved
Hide resolved
| 5. **High contrast mode:** Test in Windows High Contrast Mode or use `prefers-contrast` media query | ||
| 6. **Use browser DevTools:** Lighthouse accessibility audits can catch common issues | ||
|
|
||
| The unstyled components preserve all ARIA attributes, keyboard handling, and semantic structure. Your responsibility is to ensure custom styles don't interfere with accessibility (e.g., hiding focus indicators, insufficient contrast). |
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.
the "responsibility" effect might actually cause bad a11y for UX that users will ship for smaller JS bundle which is probably worse for user, if these things would be enforced by api then this would be more robust and safe bet to adopt.
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.
That's a valid concern, but we can't enforce something that's beyond our control. What we can do is communicate clearly and provide guidance on how it should be implemented.
|
@layershifter Thanks a lot for the detailed review and for pushing on the awkward parts of the original proposal — it helped a ton in shaping the updated RFC. Let me try to address your main concerns and also explain how the new version of the RFC responds to them. 1. Global “mode” vs. per‑component control / mutual exclusivityOur original mental model was a mutually exclusive mode switch: either “styled” or “unstyled” for the whole app, driven by a global configuration (e.g. bundler‑level configuration). That approach had some real strengths:
In practice, we realized that while this global flip is workable, it might be also too coarse‑grained and not flexible enough once you consider partial migrations and mixed ecosystems (first‑party + third‑party). The updated RFC therefore flips the default and removes the global “mode” from the proposal entirely in favor of something strictly more flexible:
This puts all emphasis on explicit imports and per‑component control, and avoids introducing a hidden global mode into the design. 2. Bundler‑based resolution isn’t ideal (and we agree)We agree that relying on bundler resolution for a global flip is not ideal: it’s toolchain‑specific, harder to debug, and can be surprising when third‑party packages are involved. In the original write‑up we leaned on it because:
In the revised RFC:
If a team still chooses to implement a bundler‑level flip on their own, that’s treated as an out‑of‑scope, toolchain‑specific customization rather than something this RFC prescribes. 3. Custom style hooks & interaction with unstyled modeYou correctly pointed out that existing Griffel‑based custom style hooks will not behave as expected when used with unstyled components in existing projects. That’s consistent with our original intent: styled and unstyled modes were designed to be mutually exclusive rather than mixed transparently. In the current design:
We’ve clarified in the RFC that:
The updated “Custom Style Hooks” and Troubleshooting sections make this explicit and set expectations that unstyled mode is not a drop‑in replacement for all existing override patterns. 4. Design tokens & CSS variables ownershipYour feedback made it clear that the original text around design tokens and CSS variables was confusing — it sounded like unstyled/headless components themselves might be responsible for providing tokens. In the revised RFC we’ve clarified:
That should resolve the ambiguity around “who is responsible for tokens” in unstyled/headless mode. 5. “Zero breaking changes” — clarified scopeYou were right to question the blanket “zero breaking changes” claim. We’ve tightened that language to be explicit about who is and isn’t affected:
The RFC now frames the change as:
6. FOUC, CSS imports, and “expected usage”You raised good concerns about:
We agree these are very real concerns, but they are also inherently tied to the consumer’s chosen styling solution rather than something Fluent can (or should) hide:
The updated RFC acknowledges these trade‑offs but positions them as trade‑offs of the chosen styling stack, not as a core defect of the unstyled/behavior design. For many teams (especially those already running a non‑Fluent design system) this is an acceptable and familiar cost. How the updated RFC addresses your concernsConcretely, the new RFC:
Thanks again for the thoughtful feedback — it significantly improved both the design and the documentation. If you see any remaining sharp edges in the updated RFC, I’m happy to iterate further. |
This is a great point and it's in tension with another goal of this RFC: avoid introducing breaking changes. In other words, we do not want to ship Fluent React v10 and want headless components to be capable or running in the same app as non-headless components. Headless will be entirely opt-in. What's proposed here leans toward a simpler implementation and ensures API compatibility at the cost of not being truly unstyled. One of the benefits of this is that the Fluent API is the Fluent API regardless of whether you use headless or headful (non-headless? styled?) Of course, user-defined headless styles my not implement the entire API which is a bit odd and can lead to unexpected bugs. Another approach that @dmytrokirpa mentioned is implementing all the unstyled components is a new package (we'll call it |
|
The following things are still on the plate. Imports
Please don't ship it under the same imports:
I.e. "Collocating unstyled components with styled components is not the right approach". This is also pure YOLO-style, are you so confident in the approach to ship it side by side with stable components? Accessibility requires stylesThere are styles that are visually hiding content: Lines 232 to 240 in bfbbdb7
No styles means = no accessibility in this context 🚨 Would be nice to understand what competitors do in this case. I personally suggest to use inline styles to be used in this case.
Architecture: hooks composabilityThe PR contains examples (thx!), I personally would like to see an example with Currently it's following: The proposal is to change button to: How it will be composed in function useBaseToggleButton(props, ref) {
const state = useBaseButtonState();
// ...
state.checked = true;
return state
}
function useToggleButton() {
// 👇 should use styled version to get styles from button
const state = useButton();
// 💥 from where comes state?
// - `useBaseToggleButton()` doesn't have styles and redoes work from `useButton()`
}Architecture: stateWho sets data attrs?Both components' examples don't have state and RFC does not explain how to handle it. What will set them? Styling
There are no examples for styling with components that have styles. A) Using data attributes get wonky with Tailwind: <Checkbox className="data-checked:border-purple-500 data-checked:p-8 ...more" />
B) For all other solutions:
Proposal: fn-based
|
ImportsAs mentioned in Teams, we can find some middle ground here - a base(headless) state hook could live alongside a component state hook in the core package, and then we could spin up some unstyled components using those base hooks in a separate package, maybe in contrib. Accessibility requires stylesFor those case we could switch to inline styles. Here is the example for ToggleButton b8edd1f#diff-b3683758a9c69a904fd6e644c7c5d6b9cf22d37507789d15819a4b8bdf3b76de Architecture: stateNo, base hooks shouldn't set any new attributes. Consumers of those hooks might set new attributes/classes based on the state if they want to. Proposal: fn-based classNameWhile I like the proposal, but I don't think it's a good time to introduce new patterns/options. I'd rather go with a previous option when Unstyled component will have note: SSR relatedAgreed, will clean it up to avoid confusion. |
e455a18 to
b8edd1f
Compare
|
Just to recap our offline chat - we're splitting this proposal into two parts:
We'll use this PR as a reference for history and examples. |




This RFC proposes headless style hook variants for Fluent UI v9 components that remove default style implementations while preserving component behavior, accessibility, and base class names. This enables teams with custom design requirements to use Fluent UI as a foundation without being forced to override or work around default styles.