-
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
base: master
Are you sure you want to change the base?
RFC: Unstyled Components #35464
Conversation
|
Pull request demo site: URL |
f81e722 to
f2b8caf
Compare
f2b8caf to
2f51180
Compare
📊 Bundle size report✅ No changes found |
| - ✅ 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 --



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.