docs/react-v9/contributing/rfcs/react-components/styles-handbook.md
This document covers how to use Griffel CSS-in-JS (used in Fluent UI React v9) to efficiently style components.
Griffel is a hybrid CSS-in-JS that features a runtime option like any other CSS-in-JS solution and Ahead-of-time compilation with CSS extraction to reduce runtime footprint and improve performance.
Griffel uses Atomic CSS to generate classes. In Atomic CSS every property-value is written as a single CSS rule.
/* Monolithic classes */
/* Can be applied only to a specific button */
.button {
display: flex;
align-items: center;
}
/* Atomic CSS */
/* Can be applied to any element that needs these rules */
.display-flex {
display: flex;
}
.align-items-center {
align-items: center;
}
Learn more about Atomic CSS.
💡 Note: All examples in this document use
@griffel/reactpackage. However, if you're a Fluent UI consumer please use@fluentui/react-componentsin imports.
makeStylesmakeStyles is used to define style permutations in components and is used for style overrides. It returns a React hook that should be called inside a component:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
button: { display: 'flex' },
icon: { paddingLeft: '5px' },
});
function Component(props) {
const classes = useClasses();
return <button className={classes.button} />;
}
makeStyles() does not support CSS shorthands in styles definitions. However, Griffel provides a set of shorthands functions to mimic them:
import { makeStyles, shorthands } from '@griffel/react';
const useClasses = makeStyles({
root: {
// ❌ This is not supported, TypeScript compiler will throw, styles will not be inserted to DOM
padding: '2px 4px 8px 16px',
// ✅ Use shorthand functions to avoid writting CSS longhands
...shorthands.padding('2px', '4px', '8px', '16px'),
},
});
💡 Note: Most of the functions follow syntax matching CSS properties, but each value should be a separate argument:
// ❌ Will produce wrong results:
// {
// paddingBottom: "2px 4px"
// paddingLeft: "2px 4px"
// paddingRight: "2px 4px"
// paddingTop: "2px 4px"
// }
shorthands.padding('2px 4px');
// ✅ Correct output:
// {
// paddingBottom: "2px"
// paddingLeft: "4px"
// paddingRight: "4px"
// paddingTop: "2px"
// }
shorthands.padding('2px', '4px');
Atomic CSS generates more classes than a standard approach with monolithic classes. Usually it's not a problem, but there are cases when performance may degrade, for example when elements have more than 100 classes. This does not happen usually as there are only a limited number of CSS properties that can be applied to a DOM element:
import { makeStyles, shorthands } from '@griffel/react';
const useClasses = makeStyles({
root: {
display: 'flex',
alignItems: 'center',
...shorthands.padding('4px'),
},
});
// ⬇️⬇️⬇️
// produces 6 classes (1 + 1 + 4 for expanded padding)
💡Tip: Preview generated classes in the Try out sandbox.
However, "CSS rule explosion" can happen when using nested selectors, pseudo classes/selectors and At-rules:
import { makeStyles, shorthands } from '@griffel/react';
const useClasses = makeStyles({
root: {
...shorthands.padding('4px'),
...shorthands.margin('4px'),
':hover': {
...shorthands.padding('4px'),
...shorthands.margin('4px'),
},
'::before': {
display: 'block',
content: "' '",
},
'@media (forced-colors: active)': {
...shorthands.padding('4px'),
...shorthands.margin('4px'),
},
},
});
// ⬇️⬇️⬇️
// produces 26 classes ((4+4)+(4+4)+2+(4+4))
Such cases might be unavoidable by design, the makeResetStyles section covers APIs to address this problem.
mergeClassesThe mergeClasses() API should be used when multiple Griffel styles are used on the same element.
import { makeStyles, shorthands } from '@griffel/react';
const useClasses = makeStyles({
root: {
/* styles */
},
foo: {
/* styles */
},
bar: {
/* styles */
},
});
function Component(props) {
const classes = useClasses();
const className = mergeClasses(
classes.root,
props.foo && classes.foo /* styles specific for "foo" */,
props.bar && classes.bar /* styles specific for "bar" */,
);
return <div className={className} />;
}
Unlike native CSS, the output of mergeClasses() is affected by the order of the classes passed in, allowing for control over priority of style overrides.
import { mergeClasses, makeStyles } from '@griffel/react';
const useClasses = makeStyles({
blue: { color: 'blue' },
red: { color: 'red' },
});
function Component(props) {
// ℹ️ Order of arguments determines the results
const redClassName = mergeClasses(classes.blue, classes.red);
// 👆 { color: 'red' }
const blueClassName = mergeClasses(classes.red, classes.blue);
// 👆 { color: 'blue' }
/* --- */
}
mergeClassesIt is not possible to simply concatenate classes returned by useClasses() hooks. Always use mergeClasses() to merge classes as results of concatenation can contain duplicated classes and lead to non-deterministic results.
import { makeStyles, mergeClasses } from '@griffel/react';
const useClasses = makeStyles({
rootA: { display: 'flex' },
rootB: { display: 'grid' },
});
function App(props) {
const classes = useClasses();
// ✅ Returns "class-display-grid"
const correctClasses = mergeClasses(classes.rootA, classes.rootB);
// 🔴 Never concatenate class strings, returns "class-display-flex class-display-grid"
const wrongClasses = classes.rootA + ' ' + classes.rootB;
}
makeResetStylesThis API works similarly to makeStyles and is used to generate styles as a single monolithic class to avoid the "CSS rules explosion" problem.
import { makeResetStyles } from '@griffel/react';
const useBaseClassname = makeResetStyles({
padding: '4px',
margin: '4px',
});
function Component(props) {
const baseClassname = useBaseClassname();
return <button className={baseClassname} />;
}
⚠️ Note: Only one class generated by
makeResetStyles()can be applied to an element. Otherwise, behavior will be non-deterministic since styles are not merged and deduplicated, the results will depend on order of insertion.
makeStyles and makeResetStyles together)We recommend using makeResetStyles API to define the base styles for a component and use makeStyles to override or enhance the base styles at runtime:
import { makeStyles, makeResetStyles, mergeClasses, shorthands } from '@griffel/react';
const useBaseClassname = makeResetStyles({
':hover': {
padding: '4px',
/* other styles */
},
'::before': {
display: 'block',
content: "' '",
},
'@media (forced-colors: active)': {
padding: '4px',
/* other styles */
},
});
// ⬇️⬇️⬇️
// produces 1 class
const useClasses = makeStyles({
circular: {
...shorthands.borderRadius('10px'),
},
primary: {
color: 'pink',
},
});
// ⬇️⬇️⬇️
// produces 4/1 classes to be conditionally applied
function Component(props) {
const baseClassName = useBaseClassname();
const classes = useClasses();
const className = mergeClasses(baseClassName, props.circular && classes.circular, props.primary && classes.primary);
return <div className={className} />;
}
makeStyles & makeResetStyles perform automatic flipping of properties and values in Right-To-Left (RTL) text direction.
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
root: {
paddingLeft: '10px',
},
});
⬇️⬇️⬇️
/* Will be applied in LTR */
.frdkuqy {
padding-left: 10px;
}
/* Will be applied in RTL */
.f81rol6 {
padding-right: 10px;
}
FluentProvider is used to determine the text direction for computed styles. The default text direction is Left-To-Right (LTR).
import { FluentProvider } from '@fluentui/react-components';
function App() {
return (
<>
<FluentProvider>
</FluentProvider>
<FluentProvider dir="rtl">
</FluentProvider>
</>
);
}
Values that contain CSS variables (or our tokens) might not be always converted, for example:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
root: {
// ⚠️ "boxShadow" will not be flipped in this example
boxShadow: 'var(--box-shadow)',
},
});
In this case, please apply your own styles with the text direction from the useFluent() hook:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
root: {
boxShadow: 'var(--box-shadow)',
},
rtl: {
boxShadow: 'var(--box-shadow-in-rtl)',
},
});
function App() {
const classes = useClasses();
const { dir } = useFluent();
const className = mergeClasses(classes.root, dir === 'rtl' && classes.rtl);
/* --- */
}
CSS Selectors are matched by browser engines from right to left (bottom-up parsing):
flowchart RL
C[li] --> B[ul] --> A[.menu]
.menu ul li {
color: #00f;
}
The browser first checks for
li, thenul, and then.menu.
It means that we need to understand the selectors that we are writing and avoid wide selectors. Let's use the component below as an example:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
test: {
color: 'orange',
'> *': {
color: 'red',
},
'> h1': {
color: 'magenta',
},
'> div': {
color: 'green',
},
'> .ui-button': {
color: 'blue',
},
},
});
function App() {
const classes = useClasses();
return (
<>
<div className={classes.test}>
<h1>Hello World</h1>
<button class="ui-button">A button</button>
</div>
{Array.from({ length: 500 }, (_, i) => (
<div key={i} />
))}
</>
);
}
Only matches the element that the class is applied to.
.fe3e8s9> * (worst)"*" matches all elements on the page.
.fzbuleu > *> h1 (non ideal)Targets all h1 tags on page.
.fohk16y > h1The performance in this example is acceptable because the page only has a single h1 tag, this will not be a case in a real app.
> div (non ideal)The selector is similar to the previous case, but now it targets all div tags. All the example contains 501 div, the performance problem is more obvious here.
.fq4d7o6 > divThis kind of selectors may not be the most efficient for pages with 1000 elements of the class 'ui-button', but it is the most effective as it targets only elements with that specific class.
.fqhvij7 > .ui-buttonIt is recommended to use this method in situations where pseudo selectors or pseudo classes are used, for example:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
test: {
':hover': {
'> .ui-button': {
color: 'blue',
},
},
},
});
tokens over direct colorsFluent UI React v9 provides design tokens for consistent theming.
import { makeStyles } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
const useClasses = makeStyles({
// ❌ Don't do
rootA: { color: 'red' /* brand foreground */ },
// ✅ Do
rootB: { color: tokens.colorBrandForeground1 },
});
Exact colors should never be used in Fluent UI code and we do not recommend their use in applications either. The exception is system-colors used by forced colors mode (inside media queries):
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
button: {
'@media (forced-colors: active)': {
color: 'ButtonText',
},
},
});
Styles written for components should follow these rules:
makeResetStyles to define themimport { makeStyles, makeResetStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
const useBaseClassName = makeResetStyles({
display: 'flex',
color: tokens.colorNeutralForeground1,
padding: '10px',
});
const useClasses = makeStyles({
// ❌ Don't do
// "display" & "padding" with the same values are defined in base styles
primary: {
display: 'flex',
...shorthands.padding('10px'),
backgroundColor: tokens.colorBrandBackground,
color: tokens.colorBrandForeground1,
},
// ✅ Do
primary: {
backgroundColor: tokens.colorBrandBackground,
color: tokens.colorBrandForeground1,
},
});
function App(props) {
const baseClassName = useBaseClassName();
const classes = useClasses();
const className = mergeClasses(baseClassName, props.primary && classes.primary);
/* --- */
}
!importantOur styles are written in way to allow predictable and simple style overrides, !important should not be necessary to override any styles:
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
const useClasses = makeStyles({
base: {
// ❌ Don't do
display: 'flex !important',
},
});
To make your code simpler, consider grouping styles that have similar conditions in mergeClasses() calls, and then apply them without the use of additional conditions at all.
import { makeStyles, makeResetStyles, mergeClasses, shorthands } from '@griffel/react';
const useBaseClassName = makeResetStyles({
display: 'flex',
fontSize: '16px',
});
const useClasses = makeStyles({
small: { fontSize: '12px' },
medium: {
/* defined in base styles */
},
large: { fontSize: '20px' },
});
function Component(props) {
const baseClassName = useBaseClassName();
const classes = useClasses();
const className = mergeClasses(baseClassName, classes[props.size]);
/* --- */
}
mergeClasses once for an elementmergeClasses is a performant function, however it's not expected that it will be called multiple times for the same element.
// ❌ Don't do
function Component(props) {
const baseClassName = useBaseClassName();
const classes = useClasses();
const classesForFoo = mergeClasses(/* ---- */);
const className = mergeClasses(baseClassName, classesForFoo, mergeClasses(/* ---- */), mergeClasses(/* ---- */));
/* --- */
}
Conditions to apply styles might be complex, in this case consider extracting them to separate variables:
// ✅ Do
function Component(props) {
const baseClassName = useBaseClassName();
const classes = useClasses();
const conditionForFoo = /* ---- */ true;
const className = mergeClasses(baseClassName, conditionForFoo && classes.foo /* other condition */);
/* --- */
}
@noflipYou can also control which rules you don't want to flip by adding a /* @noflip */ CSS comment to your rule:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
root: {
paddingLeft: '10px /* @noflip */',
},
});
⬇️⬇️⬇️
/* Will be applied in LTR & RTL */
.f6x5cb6 {
padding-left: 10px;
}
@noflip also works with shorthands.* functions:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
root: {
...shorthands.borderLeft('5px /* @noflip */', 'solid /* @noflip */', 'red /* @noflip */'),
},
});
⬇️⬇️⬇️
/* Will be applied in LTR & RTL */
.f1h8qh3y {
border-left-width: 5px;
}
.f150p1cp {
border-left-style: solid;
}
.f1sim4um {
border-left-color: red;
}
This feature can be useful to define direction specific animations:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
ltr: {
animationName: {
'0%': { left: '0% /* @noflip */' },
'100%': { left: '100% /* @noflip */' },
},
},
rtl: {
animationName: {
'100%': { right: '-100% /* @noflip */' },
'0%': { right: '100% /* @noflip */' },
},
},
});
In this case automatic flipping is disabled and will produce less CSS: 2 classes instead of 4.
Use nested selectors responsibly in styles because they can cause "CSS rule explosion".
Use nested selectors when they are combined with pseudo classes:
import { makeResetStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
const useBaseClassName = makeResetStyles({
base: {
// ✅ Do
// Shows filled icon on hover
':hover': {
[`& .${iconFilledClassName}`]: {
display: 'inline',
},
[`& .${iconRegularClassName}`]: {
display: 'none',
},
},
},
});
Do not use nested selectors to target an element or slot if you can apply classes directly on it.
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
const useClasses = makeStyles({
slotA: {
backgroundColor: tokens.colorNeutralBackground1,
// ❌ Don't do
// You can apply classes directly to that "div"
'> div': {
color: tokens.colorNeutralForeground1,
},
},
// ✅ Do
slotA: {
backgroundColor: tokens.colorNeutralBackground1,
},
slotB: {
color: tokens.colorNeutralForeground1,
},
});
function App(props) {
const classes = useClasses();
return (
<div className={classes.slotA}>
<div className={classes.slotB} />
</div>
);
}
Keep selectors simple to produce reusable CSS rules:
CSS rules that are unique cannot be reused in other areas
/* ⬇️ cannot be reused in other components */
.hash .some-unique-class {
display: flex;
}
Complicated selectors produce bigger bundle size with AOT
makeStyles({
rootA: {
display: 'flex',
},
rootB: {
'> .some-classname': {
'> .other-classname': {
display: 'flex',
alignItems: 'center',
},
},
},
});
⬇️⬇️⬇️
/* ✅ no selectors */
.f22iagw {
display: flex;
}
/* ⚠️ with complex selectors */
.f1312jvm > .some-classname > .other-classname {
display: flex;
}
.f1c58nry > .some-classname > .other-classname {
align-items: center;
}
Complicated selectors are hard to override as overrides should match component's styles
// On component's side (library code)
makeResetStyles({
'> .some-classname': {
'> .other-classname': {
':hover': {
display: 'flex',
alignItems: 'center',
},
},
},
});
// 🟡 On consumer side (application code)
// Works, but it's hard for a consumer to guess it
makeStyles({
foo: {
'> .some-classname > .other-classname:hover': {
display: 'flex',
alignItems: 'center',
},
},
});
import { makeStyles, makeResetStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
// ❌ Don't do
// Avoid complex selectors i.e. simplify them
const useBaseClassName = makeResetStyles({
'> .foo-classname': {
'> .bar-classname': {
'> .baz-classname': {
display: 'flex',
alignItems: 'center',
},
},
},
});
// ✅ Do
// Apply classes directly to an element
const useBaseBazClasses = makeResetStyles({
display: 'flex',
alignItems: 'center',
});
Instead of usage input pseudo classes in styles, prefer to use JS state.
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens } from '@fluentui/react-theme';
// ❌ Don't do
const useBaseClassName = makeResetStyles({
color: tokens.colorNeutralForeground1,
':checked': {
color: tokens.colorNeutralForeground2,
},
});
// ✅ Do
const useBaseClassName = makeResetStyles({
color: tokens.colorNeutralForeground1,
});
const useClasses = makeStyles({
checked: {
color: tokens.colorNeutralForeground2,
},
});
function Checkbox(props) {
const [checked, setChecked] = React.useState();
const baseClassName = useBaseClassName();
const classes = useClasses();
return <input className={mergeClasses(baseClassName, checked && classes.checked)} checked={checked} />;
}