Skip to content

Commit 2f51180

Browse files
committed
rfc: unstyled components
1 parent b41f392 commit 2f51180

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# RFC: Unstyled Components
2+
3+
## Contributors
4+
5+
- @dmytrokirpa
6+
7+
## Summary
8+
9+
This RFC proposes **unstyled style hook variants** that omit Griffel CSS implementations while preserving base class names (`.fui-[Component]`). This enables partners to use alternative styling solutions (CSS Modules, Tailwind, vanilla CSS) without recomposing components.
10+
11+
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.
12+
13+
**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.
14+
15+
## Problem Statement
16+
17+
Partners want to use Fluent UI v9 with alternative styling solutions but currently must:
18+
19+
1. Recompose every component manually (high maintenance)
20+
2. Override styles via `className` props (fragile, specificity issues)
21+
3. Use custom style hooks (still depends on Griffel runtime and default styles)
22+
23+
**Use cases:**
24+
25+
- Teams using CSS Modules, Tailwind CSS, or vanilla CSS
26+
- Complete design system replacement while keeping Fluent behavior/accessibility
27+
- Bundle size optimization: **~25% JS bundle size reduction** (tested on a few components) by removing Griffel runtime and style implementations
28+
29+
## Solution
30+
31+
Ship unstyled style hook variants with `.styles.unstyled.ts` extension, resolved via bundler configuration. The unstyled variant:
32+
33+
- ✅ Removes all Griffel `makeStyles`/`makeResetStyles` calls
34+
- ✅ Preserves base class names (`.fui-Button`, `.fui-Button__icon`, etc.)
35+
- ✅ Maintains identical hook signature
36+
- ✅ Component files unchanged (still supports `useCustomStyleHook_unstable`)
37+
-**~25% JS bundle size reduction** (tested) by excluding Griffel runtime
38+
39+
**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.
40+
41+
### Example
42+
43+
**Standard style hook** (`useButtonStyles.styles.ts`):
44+
45+
```tsx
46+
import { makeStyles, mergeClasses } from '@griffel/react';
47+
48+
export const buttonClassNames = { root: 'fui-Button', icon: 'fui-Button__icon' };
49+
50+
const useStyles = makeStyles({
51+
root: {
52+
/* extensive Griffel styles */
53+
},
54+
icon: {
55+
/* icon styles */
56+
},
57+
});
58+
59+
export const useButtonStyles_unstable = (state: ButtonState) => {
60+
const styles = useStyles();
61+
state.root.className = mergeClasses(buttonClassNames.root, styles.root, state.root.className);
62+
return state;
63+
};
64+
```
65+
66+
**Unstyled style hook** (`useButtonStyles.styles.unstyled.ts`):
67+
68+
```tsx
69+
import { mergeClasses } from '@fluentui/react-utilities';
70+
71+
export const buttonClassNames = { root: 'fui-Button', icon: 'fui-Button__icon' };
72+
73+
export const useButtonStyles_unstable = (state: ButtonState) => {
74+
// Only apply base class names, no styles
75+
state.root.className = mergeClasses(buttonClassNames.root, state.root.className);
76+
return state;
77+
};
78+
```
79+
80+
**Component unchanged:**
81+
82+
```tsx
83+
import { useButtonStyles_unstable } from './useButtonStyles.styles'; // ← Resolves to .unstyled.ts when configured
84+
85+
export const Button = React.forwardRef((props, ref) => {
86+
const state = useButton_unstable(props, ref);
87+
useButtonStyles_unstable(state); // ← Uses unstyled variant when configured
88+
useCustomStyleHook_unstable('useButtonStyles_unstable')(state); // ← Still available
89+
return renderButton_unstable(state);
90+
});
91+
```
92+
93+
### Bundler Configuration
94+
95+
**Webpack:**
96+
97+
```js
98+
module.exports = {
99+
resolve: { extensions: ['.unstyled.js', '...'] },
100+
};
101+
```
102+
103+
**Vite:**
104+
105+
```js
106+
export default {
107+
resolve: { extensions: ['.unstyled.js', '...'] },
108+
};
109+
```
110+
111+
**Next.js:**
112+
113+
```js
114+
module.exports = {
115+
webpack: config => {
116+
config.resolve.extensions = ['.unstyled.js', ...config.resolve.extensions];
117+
return config;
118+
},
119+
};
120+
```
121+
122+
## Implementation
123+
124+
### Option A: Statically Generated Files (Recommended)
125+
126+
Generate `.styles.unstyled.ts` files and check them into the repository.
127+
128+
**Pros:** Simple, visible in codebase, easy to verify
129+
**Cons:** Duplicate files to maintain
130+
131+
**Process:**
132+
133+
1. Scan for `use*Styles.styles.ts` files (including infrastructure components like `FluentProvider`)
134+
2. Generate `use*Styles.styles.unstyled.ts` by:
135+
- Keeping class name exports (`*ClassNames`)
136+
- Keeping CSS variable exports (for reference)
137+
- Removing all `makeStyles`/`makeResetStyles` calls
138+
- Removing Griffel imports
139+
- Simplifying hook to only apply base class names
140+
141+
### Option B: Build-Time Transform
142+
143+
Transform imports at build time via bundler plugin.
144+
145+
**Pros:** Single source of truth, automatic
146+
**Cons:** Complex build config, harder to debug
147+
148+
## Usage Examples
149+
150+
### CSS Modules
151+
152+
```css
153+
/* Button.module.css */
154+
:global(.fui-Button) {
155+
padding: 8px 16px;
156+
background-color: var(--primary-color);
157+
color: white;
158+
}
159+
```
160+
161+
### Tailwind CSS
162+
163+
```css
164+
/* Global CSS */
165+
.fui-Button {
166+
@apply px-4 py-2 bg-blue-500 text-white rounded;
167+
}
168+
```
169+
170+
### Custom Style Hook
171+
172+
```tsx
173+
<FluentProvider
174+
customStyleHooks_unstable={{
175+
useButtonStyles_unstable: useCustomButtonStyles,
176+
}}
177+
>
178+
<Button>Click me</Button>
179+
</FluentProvider>
180+
```
181+
182+
## Options Considered
183+
184+
### Option A: Unstyled Style Hooks via Extension Resolution (Chosen)
185+
186+
✅ Opt-in, zero breaking changes, follows raw modules pattern, component API unchanged
187+
👎 Requires bundler configuration
188+
189+
### Option B: Separate Package
190+
191+
✅ Clear separation, no bundler config
192+
👎 Another package to maintain, partners must change imports
193+
194+
### Option C: Runtime Flag
195+
196+
✅ No bundler config, can toggle dynamically
197+
👎 Runtime overhead, Griffel still bundled
198+
199+
## Migration
200+
201+
**For standard users:** No changes required.
202+
203+
**For unstyled users:**
204+
205+
1. Configure bundler to resolve `.unstyled.js` extensions
206+
2. Verify base class names (`.fui-*`) are applied
207+
3. Apply custom CSS targeting `.fui-*` classes
208+
4. Optionally use custom style hooks via `FluentProvider`
209+
210+
## Open Questions
211+
212+
1. **Preserve CSS variable exports?**
213+
2. **Use `mergeClasses` in unstyled hooks?**
214+
3. **Handle nested component styles?**
215+
4. **Generate for styling utility hooks?**
216+
5. **Keep unstyled variants in sync?** Automated tests + build-time validation?
217+
6. **Keep `useCustomStyleHook_unstable`?**
218+
219+
## Testing Strategy
220+
221+
- Behavioral tests (excluding style assertions)
222+
- Class name verification (`.fui-*` applied correctly)
223+
- Snapshot tests (structure identical)
224+
- Bundler integration tests (Webpack, Vite, Next.js)
225+
- Accessibility tests (ARIA, keyboard navigation)
226+
- Custom style hook tests
227+
228+
## Implementation Plan
229+
230+
### Phase 1: Proof of Concept
231+
232+
- [ ] Generate unstyled variants for 10 core components
233+
- [ ] Test with Webpack and Vite
234+
- [ ] Verify class names and custom hooks
235+
236+
### Phase 2: Build System
237+
238+
- [ ] Implement generation script
239+
- [ ] Add sync validation
240+
- [ ] Update CI
241+
242+
### Phase 3: Full Rollout
243+
244+
- [ ] Generate for all components (including infrastructure components like `FluentProvider`)
245+
- [ ] Update documentation
246+
- [ ] Add examples
247+
248+
### Phase 4: Maintenance
249+
250+
- [ ] Monitor issues
251+
- [ ] Gather feedback
252+
253+
## References
254+
255+
- [Unprocessed Styles Documentation](https://react.fluentui.dev/?path=/docs/concepts-developer-unprocessed-styles--docs)

0 commit comments

Comments
 (0)