| name | Fluent UI Griffel Styling |
| description | Deep knowledge of Griffel CSS-in-JS for Fluent UI React v9 — makeStyles, makeResetStyles, shorthands, mergeClasses, conditional styles, AOT compilation (webpack + Vite), selector performance, RTL support, icon styling, slot-based styling, CSS variables, and DevTools.
|
| allowed-tools | ["Read","Write","Edit","Glob","Grep","Bash"] |
| triggers | ["griffel","makeStyles","makeResetStyles","griffel aot","griffel webpack","griffel vite","css-in-js fluent","atomic css fluent","fluent styling","mergeClasses","fluent makeStyles","fluent griffel","griffel shorthands","griffel performance"] |
Griffel CSS-in-JS — Fluent UI React v9 Styling Engine
1. Overview
Griffel is the atomic CSS-in-JS engine that powers every Fluent UI React v9 component. It was
purpose-built by the Fluent team to solve the performance and determinism problems of traditional
CSS-in-JS libraries (styled-components, Emotion) at the scale of Microsoft 365.
Key characteristics:
- Atomic CSS output — Each CSS property/value pair becomes a single, reusable class name.
color: 'red' always produces the same atomic class everywhere it appears. This means the CSS
stylesheet grows logarithmically, not linearly, as an app scales.
- Zero-runtime with AOT — When ahead-of-time compilation is configured (webpack or Vite plugin),
Griffel extracts all styles at build time. The runtime cost drops to near zero: no style injection,
no
<style> tag management, no CSSOM mutations.
- Deterministic specificity — Because every rule is a single property in a single atomic class,
specificity conflicts are impossible. The last class in the HTML
class attribute wins, and
mergeClasses() handles the deduplication.
- RTL automatic flipping — Griffel integrates
rtl-css-js so logical properties like
paddingLeft automatically become paddingRight when the document direction is RTL. No manual
work required.
- Full TypeScript support — All style objects are fully typed. The compiler catches invalid CSS
properties and values at development time.
Core packages:
| Package | Purpose |
|---|
@griffel/react | Main runtime — makeStyles, makeResetStyles, mergeClasses, shorthands |
@griffel/webpack-extraction-plugin | AOT extraction for webpack |
@griffel/vite-plugin | AOT extraction for Vite |
@griffel/babel-preset | Babel preset for AOT (used internally by webpack/Vite plugins) |
@griffel/core | Framework-agnostic core (for non-React usage) |
@griffel/devtools | Browser extension for inspecting atomic classes |
Why not Emotion / styled-components / Tailwind?
Fluent UI chose Griffel because:
- Atomic CSS keeps stylesheet size constant regardless of component count.
- Build-time extraction eliminates runtime overhead entirely.
- Deterministic ordering removes the cascade-dependent bugs that plague Emotion.
- Full TypeScript integration catches styling errors before they ship.
- The same engine works in SSR, SSG, and CSR without hydration mismatches.
Reference:
2. Core APIs
2.1 makeStyles
makeStyles is the primary styling API. It accepts an object where each key is a named style slot,
and each value is a CSS-in-JS style object. It returns a React hook that produces class name strings.
import { makeStyles, tokens } from '@fluentui/react-components';
const useStyles = makeStyles({
root: {
display: 'flex',
alignItems: 'center',
backgroundColor: tokens.colorNeutralBackground1,
color: tokens.colorNeutralForeground1,
},
icon: {
fontSize: '20px',
marginRight: tokens.spacingHorizontalS,
},
label: {
fontWeight: tokens.fontWeightSemibold,
lineHeight: tokens.lineHeightBase300,
},
});
function MyComponent() {
const styles = useStyles();
return (
<div className={styles.root}>
<span className={styles.icon}>*</span>
<span className={styles.label}>Hello</span>
</div>
);
}
How it works internally:
- At call time (or build time with AOT), Griffel converts each CSS property/value pair into a
deterministic hash-based class name.
- The hook returns an object where each key maps to a space-separated string of atomic class names.
- Applying
styles.root to className inserts all the atomic classes for that slot.
Supported style properties:
All standard CSS properties are supported in camelCase form:
- Layout:
display, position, top, right, bottom, left, zIndex, float, clear
- Flexbox:
flexDirection, flexWrap, justifyContent, alignItems, alignSelf, flexGrow,
flexShrink, flexBasis, order
- Grid:
gridTemplateColumns, gridTemplateRows, gridColumn, gridRow, gridGap,
gridAutoFlow, gridAutoColumns, gridAutoRows
- Box model:
width, height, minWidth, maxWidth, minHeight, maxHeight, boxSizing
- Typography:
fontSize, fontWeight, fontFamily, lineHeight, letterSpacing,
textAlign, textTransform, textDecoration, whiteSpace, wordBreak, overflowWrap
- Visual:
color, backgroundColor, opacity, visibility, cursor, pointerEvents
- Borders:
borderTopWidth, borderTopStyle, borderTopColor (and all sides)
- Spacing:
paddingTop, paddingRight, paddingBottom, paddingLeft, marginTop, etc.
- Shadows:
boxShadow, textShadow
- Transforms:
transform, transformOrigin, transition, transitionDuration,
transitionProperty, transitionTimingFunction
- Animations:
animationName, animationDuration, animationTimingFunction,
animationIterationCount, animationDirection, animationFillMode
- Overflow:
overflowX, overflowY
- Miscellaneous:
outline, outlineWidth, outlineStyle, outlineColor, outlineOffset,
listStyleType, content, userSelect, scrollBehavior, resize
2.2 makeResetStyles
makeResetStyles creates a single non-atomic class that applies a bundle of styles together.
Use it for base/reset styles where atomicity is unnecessary overhead.
import { makeResetStyles, tokens } from '@fluentui/react-components';
const useBaseClass = makeResetStyles({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '44px',
padding: '0 16px',
borderRadius: tokens.borderRadiusMedium,
fontFamily: tokens.fontFamilyBase,
fontSize: tokens.fontSizeBase300,
lineHeight: tokens.lineHeightBase300,
backgroundColor: tokens.colorNeutralBackground1,
color: tokens.colorNeutralForeground1,
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`,
cursor: 'pointer',
transitionDuration: tokens.durationNormal,
transitionProperty: 'background-color, border-color, color',
':hover': {
backgroundColor: tokens.colorNeutralBackground1Hover,
},
':active': {
backgroundColor: tokens.colorNeutralBackground1Pressed,
},
});
When to use makeResetStyles vs makeStyles:
| Scenario | Use |
|---|
| Component variant styles applied conditionally | makeStyles |
| Base reset/normalization styles always applied | makeResetStyles |
Styles that other components will override via mergeClasses | makeStyles (atomic wins) |
| Complex selectors with many properties in a single rule | makeResetStyles |
Critical difference: makeResetStyles outputs a single class with all properties. This means
it has lower specificity than atomic makeStyles classes (which each target one property). When
you mergeClasses(baseClass, atomicStyles.variant), the atomic class will override the reset class
for any conflicting properties. This is by design and is the recommended composition pattern.
2.3 shorthands
CSS shorthands like padding: '8px 16px' cannot be decomposed into atomic classes because they set
multiple properties at once. Griffel provides shorthands.* functions that expand them into their
longhand equivalents so each can become its own atomic class.
import { makeStyles, shorthands } from '@fluentui/react-components';
const useStyles = makeStyles({
root: {
...shorthands.padding('8px', '16px'),
...shorthands.border('1px', 'solid', 'red'),
...shorthands.borderRadius('4px'),
},
});
Complete shorthands.* function list:
| Function | Expands to | Signature |
|---|
shorthands.border(width, style, color) | All border longhand props | (width?, style?, color?) |
shorthands.borderLeft(width, style, color) | borderLeftWidth, borderLeftStyle, borderLeftColor | (width?, style?, color?) |
shorthands.borderRight(width, style, color) | Same for right side | (width?, style?, color?) |
shorthands.borderTop(width, style, color) | Same for top side | (width?, style?, color?) |
shorthands.borderBottom(width, style, color) | Same for bottom side | (width?, style?, color?) |
shorthands.borderColor(top, right, bottom, left) | All four border*Color props | (top, right?, bottom?, left?) |
shorthands.borderStyle(top, right, bottom, left) | All four border*Style props | (top, right?, bottom?, left?) |
shorthands.borderWidth(top, right, bottom, left) | All four border*Width props | (top, right?, bottom?, left?) |
shorthands.borderRadius(topLeft, topRight, bottomRight, bottomLeft) | All four border*Radius props | (topLeft, topRight?, bottomRight?, bottomLeft?) |
shorthands.padding(top, right, bottom, left) | All four padding* props | (top, right?, bottom?, left?) |
shorthands.margin(top, right, bottom, left) | All four margin* props | (top, right?, bottom?, left?) |
shorthands.gap(row, column) | rowGap, columnGap | (row, column?) |
shorthands.overflow(x, y) | overflowX, overflowY | (x, y?) |
shorthands.flex(grow, shrink, basis) | flexGrow, flexShrink, flexBasis | (grow, shrink?, basis?) |
shorthands.gridArea(rowStart, colStart, rowEnd, colEnd) | Grid placement props | (rowStart, colStart?, rowEnd?, colEnd?) |
shorthands.inset(top, right, bottom, left) | top, right, bottom, left | (top, right?, bottom?, left?) |
shorthands.outline(width, style, color) | outlineWidth, outlineStyle, outlineColor | (width?, style?, color?) |
shorthands.textDecoration(line, style, color, thickness) | Text decoration longhands | (line, style?, color?, thickness?) |
shorthands.transition(property, duration, timingFunction, delay) | Transition longhands | (property, duration?, timingFunction?, delay?) |
Argument pattern: All shorthands.* functions follow CSS shorthand argument conventions:
- 1 arg: applies to all sides
- 2 args: vertical/horizontal
- 3 args: top / horizontal / bottom
- 4 args: top / right / bottom / left
2.4 mergeClasses
mergeClasses deduplicates and composes atomic class names. It is the only correct way to combine
Griffel class names.
import { mergeClasses } from '@fluentui/react-components';
function MyButton({ primary, disabled, className }: Props) {
const styles = useStyles();
const baseClass = useBaseClass();
return (
<button
className={mergeClasses(
baseClass,
styles.root,
primary && styles.primary,
disabled && styles.disabled,
className // Allow parent to override styles
)}
/>
);
}
How mergeClasses works:
- It receives any number of arguments (strings, undefined, false, null — falsy values are skipped).
- For each atomic class, it tracks which CSS property it targets.
- When two classes target the same property, the later one wins (last-write-wins semantics).
- It returns a single, deduplicated space-separated class string.
Critical rules:
- Never concatenate strings:
`${styles.root} ${styles.icon}` breaks deduplication.
Always use mergeClasses(styles.root, styles.icon).
- Use
mergeClasses only once per element: Do not nest mergeClasses(mergeClasses(...), ...).
Pass all classes to a single call.
- Always include the external
className prop last so parent components can override child styles.
3. Nine Critical Rules for Griffel
These are the non-negotiable rules for correct Griffel usage. Violating any of them produces bugs
that are difficult to diagnose.
Rule 1: Never Concatenate Classes — Always mergeClasses()
<div className={`${styles.root} ${styles.active}`} />
<div className={styles.root + ' ' + styles.active} />
<div className={[styles.root, styles.active].join(' ')} />
<div className={mergeClasses(styles.root, styles.active)} />
When you concatenate strings, two atomic classes targeting the same CSS property (e.g., color)
both remain in the class list. Which one wins depends on their insertion order in the stylesheet,
which is non-deterministic. mergeClasses resolves this by keeping only the last class for each
property.
Rule 2: Avoid !important
!important defeats the entire atomic CSS model. It cannot be overridden by mergeClasses,
it breaks component composition, and it prevents parent components from customizing child styles.
const useStyles = makeStyles({
root: {
color: 'red !important',
},
});
<div className={mergeClasses(styles.base, styles.override)} />
Rule 3: Use Tokens Over Direct Colors
Hardcoded colors break theming, dark mode, and high-contrast mode. Always use Fluent design tokens.
const useStyles = makeStyles({
root: {
color: '#333333',
backgroundColor: 'white',
},
});
const useStyles = makeStyles({
root: {
color: tokens.colorNeutralForeground1,
backgroundColor: tokens.colorNeutralBackground1,
},
});
Rule 4: Avoid Rule Duplication Across Style Objects
Griffel generates one atomic class per unique property/value pair. Duplicating the same property/value
across multiple named slots wastes no CSS bytes (it is the same atomic class), but it clutters your
code and makes maintenance harder.
const useStyles = makeStyles({
title: { fontSize: '14px', fontWeight: 'bold' },
subtitle: { fontSize: '14px', fontWeight: 'normal' },
});
const useStyles = makeStyles({
text: { fontSize: '14px' },
bold: { fontWeight: 'bold' },
normal: { fontWeight: 'normal' },
});
<span className={mergeClasses(styles.text, styles.bold)}>Title</span>
<span className={mergeClasses(styles.text, styles.normal)}>Subtitle</span>
Rule 5: Use mergeClasses Only Once Per Element
Nesting mergeClasses calls produces correct output but is wasteful. Flatten all classes into
a single call.
<div className={mergeClasses(mergeClasses(base, styles.root), styles.active)} />
<div className={mergeClasses(base, styles.root, styles.active)} />
Rule 6: No CSS Shorthands in makeStyles — Use shorthands.*
CSS shorthands in makeStyles cannot be decomposed into atomic classes. They will either fail
silently or produce incorrect output.
const useStyles = makeStyles({
root: {
padding: '8px 16px',
border: '1px solid #ccc',
borderRadius: '4px',
},
});
const useStyles = makeStyles({
root: {
...shorthands.padding('8px', '16px'),
...shorthands.border('1px', 'solid', '#ccc'),
...shorthands.borderRadius('4px'),
},
});
Exception: makeResetStyles does accept CSS shorthands because it produces a non-atomic
single class.
Rule 7: Tag Selectors Are Slow — Prefer className Props
Never style elements by tag name in Griffel selectors. Tag selectors (> div, > span) are slow
and fragile. Instead, pass className directly to child components or use slot objects.
const useStyles = makeStyles({
root: {
'> div': { color: 'red' },
'> svg': { fill: 'blue' },
},
});
const useStyles = makeStyles({
root: { },
childDiv: { color: 'red' },
childIcon: { color: 'blue' },
});
<div className={styles.root}>
<div className={styles.childDiv}>...</div>
<MyIcon className={styles.childIcon} />
</div>
Rule 8: RTL Auto-Flips Automatically
Griffel uses rtl-css-js to automatically flip directional properties when the document direction
is RTL. You do not need (and should not add) manual RTL overrides.
const useStyles = makeStyles({
root: {
paddingLeft: '16px',
marginRight: '8px',
textAlign: 'left',
float: 'left',
borderLeftWidth: '2px',
},
});
Properties that auto-flip:
paddingLeft / paddingRight
marginLeft / marginRight
borderLeftWidth / borderRightWidth (and style, color)
left / right
textAlign: 'left' / 'right'
float: 'left' / 'right'
borderTopLeftRadius / borderTopRightRadius (and bottom)
transform: translateX(...) (value is negated)
To enable RTL, set dir="rtl" on FluentProvider:
<FluentProvider theme={teamsLightTheme} dir="rtl">
<App />
</FluentProvider>
Rule 9: AOT Compilation Eliminates Runtime Overhead
When configured, Griffel's ahead-of-time plugins extract all makeStyles and makeResetStyles calls
at build time. The resulting CSS is injected via standard <link> or <style> tags, and the runtime
JavaScript for style computation is tree-shaken away.
See Section 6 below for full configuration details.
4. Conditional Styles
The recommended pattern for variant/state/size-based styling is to define each variant as a separate
slot in makeStyles and compose them with mergeClasses.
Variant-based pattern
import { makeStyles, mergeClasses, tokens } from '@fluentui/react-components';
import type { ButtonProps } from './types';
const useStyles = makeStyles({
root: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '32px',
},
primary: {
backgroundColor: tokens.colorBrandBackground,
color: tokens.colorNeutralForegroundOnBrand,
':hover': {
backgroundColor: tokens.colorBrandBackgroundHover,
},
':active': {
backgroundColor: tokens.colorBrandBackgroundPressed,
},
},
secondary: {
backgroundColor: tokens.colorNeutralBackground1,
color: tokens.colorNeutralForeground1,
':hover': {
backgroundColor: tokens.colorNeutralBackground1Hover,
},
},
subtle: {
backgroundColor: 'transparent',
color: tokens.colorNeutralForeground2,
':hover': {
backgroundColor: tokens.colorSubtleBackgroundHover,
},
},
small: {
minHeight: '24px',
fontSize: tokens.fontSizeBase200,
...shorthands.padding('0', tokens.spacingHorizontalS),
},
medium: {
minHeight: '32px',
fontSize: tokens.fontSizeBase300,
...shorthands.padding('0', tokens.spacingHorizontalM),
},
large: {
minHeight: '40px',
fontSize: tokens.fontSizeBase400,
...shorthands.padding('0', tokens.spacingHorizontalL),
},
disabled: {
backgroundColor: tokens.colorNeutralBackgroundDisabled,
color: tokens.colorNeutralForegroundDisabled,
cursor: 'not-allowed',
':hover': {
backgroundColor: tokens.colorNeutralBackgroundDisabled,
},
},
});
function MyButton({
appearance = 'secondary',
size = 'medium',
disabled = false,
className,
children,
}: ButtonProps) {
const styles = useStyles();
const appearanceStyles: Record<string, string> = {
primary: styles.primary,
secondary: styles.secondary,
subtle: styles.subtle,
};
const sizeStyles: Record<string, string> = {
small: styles.small,
medium: styles.medium,
large: styles.large,
};
return (
<button
className={mergeClasses(
styles.root,
appearanceStyles[appearance],
sizeStyles[size],
disabled && styles.disabled,
className
)}
disabled={disabled}
>
{children}
</button>
);
}
Boolean state pattern
const useStyles = makeStyles({
root: { },
selected: {
backgroundColor: tokens.colorNeutralBackground1Selected,
fontWeight: tokens.fontWeightSemibold,
},
focused: {
outlineWidth: '2px',
outlineStyle: 'solid',
outlineColor: tokens.colorStrokeFocus2,
},
});
function ListItem({ selected, focused, className }: Props) {
const styles = useStyles();
return (
<div
className={mergeClasses(
styles.root,
selected && styles.selected,
focused && styles.focused,
className
)}
/>
);
}
5. Media Queries and Keyframes
Media queries
Media queries are supported as nested objects within makeStyles:
const useStyles = makeStyles({
root: {
display: 'flex',
flexDirection: 'column',
'@media (min-width: 640px)': {
flexDirection: 'row',
},
'@media (min-width: 1024px)': {
maxWidth: '960px',
marginLeft: 'auto',
marginRight: 'auto',
},
'@media (prefers-reduced-motion: reduce)': {
transitionDuration: '0.01ms',
animationDuration: '0.01ms',
},
'@media (prefers-color-scheme: dark)': {
colorScheme: 'dark',
},
'@media print': {
backgroundColor: 'white',
color: 'black',
},
},
});
@supports queries
const useStyles = makeStyles({
root: {
display: 'flex',
'@supports (display: grid)': {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
},
},
});
Keyframe animations
animationName accepts a keyframe object (or array of keyframe objects) instead of a string:
const useStyles = makeStyles({
fadeIn: {
animationName: {
from: { opacity: 0, transform: 'translateY(-8px)' },
to: { opacity: 1, transform: 'translateY(0)' },
},
animationDuration: tokens.durationNormal,
animationTimingFunction: tokens.curveDecelerateMax,
animationFillMode: 'forwards',
},
spin: {
animationName: {
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' },
},
animationDuration: '1s',
animationTimingFunction: 'linear',
animationIterationCount: 'infinite',
},
pulseAndFade: {
animationName: [
{
'0%': { opacity: 0, transform: 'scale(0.95)' },
'50%': { opacity: 1, transform: 'scale(1.02)' },
'100%': { opacity: 1, transform: 'scale(1)' },
},
],
animationDuration: tokens.durationUltraSlow,
animationTimingFunction: tokens.curveDecelerateMax,
animationFillMode: 'forwards',
},
});
6. AOT Compilation
Ahead-of-time (AOT) compilation is the most impactful optimization for Griffel. It moves all style
computation from runtime to build time, producing static CSS that is injected via <style> tags
rather than CSSOM APIs.
Reference: https://griffel.js.org/react/ahead-of-time-compilation/introduction/
Benefits of AOT
- Zero runtime style insertion — No
CSSStyleSheet.insertRule() calls, no CSSOM mutations.
- Smaller JS bundles — Style objects are replaced with pre-computed class name maps. The Griffel
runtime is tree-shaken to a minimal size.
- Faster hydration — SSR pages hydrate faster because there is no style re-computation.
- Consistent performance — Style computation cost is paid once at build time, not on every render.
- Reduced memory — No in-memory style cache needed at runtime.
Webpack Configuration
Reference: https://griffel.js.org/react/ahead-of-time-compilation/with-webpack/
npm install --save-dev @griffel/webpack-extraction-plugin
const { GriffelCSSExtractionPlugin } = require('@griffel/webpack-extraction-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: '@griffel/webpack-extraction-plugin/loader',
options: {
},
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new GriffelCSSExtractionPlugin(),
new MiniCssExtractPlugin(),
],
};
For Next.js with webpack, see the fluent-nextjs skill which covers the Next.js-specific
configuration including withGriffelCSSExtraction.
Vite Configuration
Reference: https://griffel.js.org/react/ahead-of-time-compilation/with-vite/
npm install --save-dev @griffel/vite-plugin
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import griffel from '@griffel/vite-plugin';
export default defineConfig({
plugins: [
griffel(),
react(),
],
});
Vite plugin options:
griffel({
babelOptions: {
plugins: [],
presets: [],
},
modules: [
{ moduleSource: '@fluentui/react-components', importName: 'makeStyles' },
{ moduleSource: '@griffel/react', importName: 'makeStyles' },
],
});
How AOT Works Internally
- Parse phase — The Babel preset finds all
makeStyles() and makeResetStyles() calls.
- Evaluate phase — Style objects are statically evaluated. Token references (CSS variables)
are preserved as-is; they resolve at runtime through the
FluentProvider.
- Transform phase — Each style object is converted to its atomic class names. The original
makeStyles() call is replaced with a pre-computed class name lookup.
- Extract phase — The generated CSS rules are extracted into
.css assets that the bundler
processes normally (via css-loader + MiniCssExtractPlugin in webpack, or native CSS handling
in Vite).
Before AOT (source):
const useStyles = makeStyles({
root: {
color: tokens.colorNeutralForeground1,
display: 'flex',
},
});
After AOT (transformed output):
const useStyles = __styles({
root: {
sj55zd: 'f1a7mspl',
mc9l5x: 'f22iagw',
},
}, );
7. Selector Performance
The following guidance is based on the Fluent UI team's performance work, including insights from
Tiger Oakes' analysis of CSS performance patterns in Fluent UI (https://tigeroakes.com/posts/fluent-ui-css-tricks/).
Slot-based styling (prefer over child selectors)
Fluent UI components expose styling through slot objects, not child selectors. Each slot accepts
a className property that lets you style it directly.
const useStyles = makeStyles({
input: {
'> .fui-Input__contentBefore': {
color: 'red',
},
},
});
<Input
contentBefore={{
className: styles.inputIcon,
children: <SearchIcon />,
}}
/>
Key Fluent v9 components with important slots:
| Component | Notable slots |
|---|
Input | root, input, contentBefore, contentAfter |
Button | root, icon |
Menu | root, through MenuList, MenuItem |
Dialog | root, through DialogSurface, DialogTitle, DialogBody, DialogActions |
Card | root, through CardHeader, CardPreview, CardFooter |
DataGrid | root, through column-level and cell-level components |
Combobox | root, input, listbox, expandIcon |
TabList | root, through individual Tab components |
Icon styling
Fluent icons use fill="currentColor" in their SVG markup. This means you style icon color with
the CSS color property, not fill.
const useStyles = makeStyles({
icon: {
fill: 'red',
},
});
const useStyles = makeStyles({
icon: {
color: tokens.colorBrandForeground1,
fontSize: '24px',
},
});
Avoid tag selectors
Tag selectors (> div, > span, > svg) match by element type, which is slower than class
selectors and brittle when component internals change.
const useStyles = makeStyles({
root: {
'> div': {
padding: '4px',
},
},
});
const useStyles = makeStyles({
root: { },
child: { ...shorthands.padding('4px') },
});
Pseudo-selector performance
Griffel supports all CSS pseudo-selectors:
const useStyles = makeStyles({
root: {
':hover': {
backgroundColor: tokens.colorNeutralBackground1Hover,
},
':active': {
backgroundColor: tokens.colorNeutralBackground1Pressed,
},
':focus-visible': {
outlineWidth: '2px',
outlineStyle: 'solid',
outlineColor: tokens.colorStrokeFocus2,
outlineOffset: '2px',
},
':first-child': {
marginTop: '0',
},
':last-child': {
marginBottom: '0',
},
'::before': {
content: '""',
display: 'block',
},
'::after': {
content: '""',
position: 'absolute',
},
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
pointerEvents: 'none',
},
},
});
8. RTL Support
Griffel provides automatic RTL support through its integration with rtl-css-js. When the
FluentProvider has dir="rtl", all directional CSS properties are automatically flipped.
Setup
import { FluentProvider, teamsLightTheme } from '@fluentui/react-components';
function App() {
const [dir, setDir] = useState<'ltr' | 'rtl'>('ltr');
return (
<FluentProvider theme={teamsLightTheme} dir={dir}>
<YourApp />
</FluentProvider>
);
}
What flips automatically
| LTR property | RTL equivalent |
|---|
paddingLeft: '16px' | paddingRight: '16px' |
marginLeft: '8px' | marginRight: '8px' |
borderLeftWidth: '2px' | borderRightWidth: '2px' |
left: '0' | right: '0' |
textAlign: 'left' | textAlign: 'right' |
float: 'left' | float: 'right' |
transform: 'translateX(10px)' | transform: 'translateX(-10px)' |
borderTopLeftRadius | borderTopRightRadius |
Properties that do NOT flip
Some properties are intentionally not flipped:
direction — Already controls the text direction
unicode-bidi — Already controls bidi behavior
- Physical properties like
borderBlockStart, inlineStart (CSS logical properties)
Mixed-direction content
For content that should NOT flip (e.g., LTR code blocks inside an RTL page):
<FluentProvider dir="rtl" theme={teamsLightTheme}>
<MainContent />
<FluentProvider dir="ltr">
<CodeBlock /> {/* This content stays LTR */}
</FluentProvider>
</FluentProvider>
9. Dynamic Styles with CSS Variables
For styles that must change at runtime based on props or state (e.g., a progress bar width, a
user-selected color), use CSS custom properties rather than inline styles. This preserves Griffel's
atomic caching.
Pattern: CSS variables for dynamic values
import { makeStyles, mergeClasses, tokens } from '@fluentui/react-components';
const useStyles = makeStyles({
progressBar: {
height: '4px',
backgroundColor: tokens.colorNeutralBackground6,
borderRadius: tokens.borderRadiusSmall,
position: 'relative',
overflow: 'hidden',
},
progressFill: {
position: 'absolute',
top: '0',
left: '0',
height: '100%',
backgroundColor: tokens.colorBrandBackground,
width: 'var(--progress-width, 0%)',
transitionProperty: 'width',
transitionDuration: tokens.durationNormal,
transitionTimingFunction: tokens.curveDecelerateMax,
},
});
function ProgressBar({ value }: { value: number }) {
const styles = useStyles();
const clampedValue = Math.min(100, Math.max(0, value));
return (
<div className={styles.progressBar}>
<div
className={styles.progressFill}
style={{ '--progress-width': `${clampedValue}%` } as React.CSSProperties}
/>
</div>
);
}
Pattern: Dynamic color from props
const useStyles = makeStyles({
badge: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '20px',
height: '20px',
borderRadius: tokens.borderRadiusCircular,
fontSize: tokens.fontSizeBase100,
fontWeight: tokens.fontWeightSemibold,
backgroundColor: 'var(--badge-bg)',
color: 'var(--badge-fg)',
},
});
function ColorBadge({ bg, fg, children }: { bg: string; fg: string; children: React.ReactNode }) {
const styles = useStyles();
return (
<span
className={styles.badge}
style={{
'--badge-bg': bg,
'--badge-fg': fg,
} as React.CSSProperties}
>
{children}
</span>
);
}
When to use CSS variables vs. conditional styles
| Scenario | Approach |
|---|
| Finite set of variants (primary, secondary, subtle) | Conditional styles via mergeClasses |
| Value from a prop (width, height, color from data) | CSS variables |
| Theme-dependent values | Design tokens (already CSS variables) |
| Animated values | CSS variables + transitions |
10. Shared Style Libraries
For design systems built on top of Fluent, you can create shared style packages that multiple
apps consume.
Creating a shared style library
export { useCardStyles } from './card-styles';
export { useFormFieldStyles } from './form-field-styles';
export { usePageLayoutStyles } from './page-layout-styles';
import { makeStyles, shorthands, tokens } from '@fluentui/react-components';
export const useCardStyles = makeStyles({
root: {
display: 'flex',
flexDirection: 'column',
backgroundColor: tokens.colorNeutralBackground1,
...shorthands.borderRadius(tokens.borderRadiusMedium),
...shorthands.padding(tokens.spacingVerticalL, tokens.spacingHorizontalL),
boxShadow: tokens.shadow4,
},
header: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: tokens.spacingVerticalM,
},
body: {
flexGrow: 1,
},
footer: {
display: 'flex',
justifyContent: 'flex-end',
...shorthands.gap(tokens.spacingHorizontalS),
marginTop: tokens.spacingVerticalM,
},
});
Consuming shared styles
import { mergeClasses } from '@fluentui/react-components';
import { useCardStyles } from '@my-org/shared-styles';
const useLocalStyles = makeStyles({
projectCard: {
minHeight: '200px',
},
highlighted: {
boxShadow: tokens.shadow8Brand,
},
});
function ProjectCard({ highlighted, className }: Props) {
const cardStyles = useCardStyles();
const localStyles = useLocalStyles();
return (
<div
className={mergeClasses(
cardStyles.root,
localStyles.projectCard,
highlighted && localStyles.highlighted,
className
)}
>
<div className={cardStyles.header}>...</div>
<div className={cardStyles.body}>...</div>
<div className={cardStyles.footer}>...</div>
</div>
);
}
AOT with shared libraries
When using AOT compilation, shared style libraries must be processed by the Griffel plugin too.
In webpack, ensure the shared package is not excluded from the loader:
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules\/(?!@my-org\/shared-styles)/,
use: {
loader: '@griffel/webpack-extraction-plugin/loader',
},
},
11. DevTools Extension
The Griffel DevTools browser extension helps debug atomic CSS by mapping class names back to their
source makeStyles calls.
Installation
Install from the Chrome Web Store or Firefox Add-ons: search for "Griffel DevTools".
Features
- Class name inspector — Hover over an element to see which atomic classes are applied and what
CSS property/value each represents.
- Source mapping — Click an atomic class to jump to the
makeStyles call that generated it.
- Override detection — See which classes were overridden by
mergeClasses (shown as
strikethrough).
- Performance metrics — View runtime style insertion counts and timing.
Debugging without DevTools
Without the extension, you can still debug Griffel by:
- Reading atomic class names — Each class follows the pattern
f{hash}. In browser DevTools,
inspect the element's Computed Styles to see which rules apply.
- Searching source — The CSS property targeted by an atomic class can be found by searching the
<style> tags in the document head for the class name.
- Logging
mergeClasses output — Log the return value to see the final class string after
deduplication.
12. Cross-References
| Topic | Skill |
|---|
| AOT compilation with Next.js (App Router + Pages Router) | fluent-nextjs |
| Slot-based styling and component composition | fluent-extensibility |
| Design tokens and theming | fluent-design-system |
| Form component styling patterns | fluent-forms |
| Charting component styling | fluent-charting |
| Web Component styling (non-React) | fluent-web-components |
| Cross-platform styling considerations | fluent-cross-platform |
| Integration with third-party libraries | fluent-integration |