Back to Fluentui

Styles Handbook

docs/react-v9/contributing/rfcs/react-components/styles-handbook.md

4.40.2-hotfix224.9 KB
Original Source

Styles Handbook

@layeshifter

This document covers how to use Griffel CSS-in-JS (used in Fluent UI React v9) to efficiently style components.

Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->

Introduction

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.

Core concepts

Atomic CSS

Griffel uses Atomic CSS to generate classes. In Atomic CSS every property-value is written as a single CSS rule.

css
/* 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/react package. However, if you're a Fluent UI consumer please use @fluentui/react-components in imports.

makeStyles

makeStyles 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:

js
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} />;
}

Limitations

makeStyles() does not support CSS shorthands in styles definitions. However, Griffel provides a set of shorthands functions to mimic them:

js
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:

js
// ❌ 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');

Performance caveat

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:

js
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:

js
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.

mergeClasses

The mergeClasses() API should be used when multiple Griffel styles are used on the same element.

js
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} />;
}

Order of arguments determines results

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.

js
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' }

  /* --- */
}

⚠️ Only combine classes with mergeClasses

It 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.

js
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;
}

makeResetStyles

This API works similarly to makeStyles and is used to generate styles as a single monolithic class to avoid the "CSS rules explosion" problem.

js
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.

Hybrid approach (Using 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:

jsx
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} />;
}

RTL styles

makeStyles & makeResetStyles perform automatic flipping of properties and values in Right-To-Left (RTL) text direction.

js
import { makeStyles } from '@griffel/react';

const useClasses = makeStyles({
  root: {
    paddingLeft: '10px',
  },
});

⬇️⬇️⬇️

css
/* 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).

jsx
import { FluentProvider } from '@fluentui/react-components';

function App() {
  return (
    <>
      <FluentProvider>
      </FluentProvider>
      <FluentProvider dir="rtl">
      </FluentProvider>
    </>
  );
}

CSS variables caveat

Values that contain CSS variables (or our tokens) might not be always converted, for example:

js
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:

js
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);

  /* --- */
}

Understanding selector complexity

CSS Selectors are matched by browser engines from right to left (bottom-up parsing):

mermaid
flowchart RL
    C[li] --> B[ul] --> A[.menu]
css
.menu ul li {
  color: #00f;
}

The browser first checks for li, then ul, 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:

jsx
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} />
      ))}
    </>
  );
}
No selector (best)

Only matches the element that the class is applied to.

  • selector .fe3e8s9
  • match_attempts 1
  • match_count 1
> * (worst)

"*" matches all elements on the page.

  • selector .fzbuleu > *
  • match_attempts 503
  • match_count 2
> h1 (non ideal)

Targets all h1 tags on page.

  • selector .fohk16y > h1
  • match_attempts 1
  • match_count 1

The 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.

  • selector .fq4d7o6 > div
  • match_attempts 501
  • match_count 1

Targeting a classname (better)

This 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.

  • selector .fqhvij7 > .ui-button
  • match_attempts 1
  • match_count 1

It is recommended to use this method in situations where pseudo selectors or pseudo classes are used, for example:

js
import { makeStyles } from '@griffel/react';

const useClasses = makeStyles({
  test: {
    ':hover': {
      '> .ui-button': {
        color: 'blue',
      },
    },
  },
});

Best practices

Writing styles

Use tokens over direct colors

Fluent UI React v9 provides design tokens for consistent theming.

js
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):

js
import { makeStyles } from '@griffel/react';

const useClasses = makeStyles({
  button: {
    '@media (forced-colors: active)': {
      color: 'ButtonText',
    },
  },
});

Avoid rule duplication

Styles written for components should follow these rules:

  • base styles should contain most of the CSS definitions that are applicable to base state
    • use makeResetStyles to define them
  • styles for permutations should be granular
  • base styles should not be duplicated by permutations
js
import { 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);

  /* --- */
}

Avoid !important

Our styles are written in way to allow predictable and simple style overrides, !important should not be necessary to override any styles:

js
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';

const useClasses = makeStyles({
  base: {
    // ❌ Don't do
    display: 'flex !important',
  },
});

Use structured styles

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.

js
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]);

  /* --- */
}

Performance

Use mergeClasses once for an element

mergeClasses is a performant function, however it's not expected that it will be called multiple times for the same element.

js
// ❌ 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:

js
// ✅ Do
function Component(props) {
  const baseClassName = useBaseClassName();
  const classes = useClasses();

  const conditionForFoo = /* ---- */ true;
  const className = mergeClasses(baseClassName, conditionForFoo && classes.foo /* other condition */);

  /* --- */
}

Avoid unnecessary RTL transforms by using @noflip

You can also control which rules you don't want to flip by adding a /* @noflip */ CSS comment to your rule:

js
import { makeStyles } from '@griffel/react';

const useClasses = makeStyles({
  root: {
    paddingLeft: '10px /* @noflip */',
  },
});

⬇️⬇️⬇️

css
/* Will be applied in LTR & RTL */
.f6x5cb6 {
  padding-left: 10px;
}

@noflip also works with shorthands.* functions:

js
import { makeStyles } from '@griffel/react';

const useClasses = makeStyles({
  root: {
    ...shorthands.borderLeft('5px /* @noflip */', 'solid /* @noflip */', 'red /* @noflip */'),
  },
});

⬇️⬇️⬇️

css
/* 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:

js
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.

Nested selectors

Use nested selectors responsibly in styles because they can cause "CSS rule explosion".

Use nested selectors with pseudo classes

Use nested selectors when they are combined with pseudo classes:

js
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',
      },
    },
  },
});

Apply classes directly to elements

Do not use nested selectors to target an element or slot if you can apply classes directly on it.

jsx
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>
  );
}

Avoid complicated selectors

Keep selectors simple to produce reusable CSS rules:

  • CSS rules that are unique cannot be reused in other areas

    css
    /*    ⬇️ cannot be reused in other components  */
    .hash .some-unique-class {
      display: flex;
    }
    
  • Complicated selectors produce bigger bundle size with AOT

    js
    makeStyles({
      rootA: {
        display: 'flex',
      },
      rootB: {
        '> .some-classname': {
          '> .other-classname': {
            display: 'flex',
            alignItems: 'center',
          },
        },
      },
    });
    

    ⬇️⬇️⬇️

    css
    /* ✅ 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

    js
    // 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',
        },
      },
    });
    
js
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',
});

Avoid input pseudo classes

Instead of usage input pseudo classes in styles, prefer to use JS state.

  • Produces less classes on an element
  • Selectors for overrides are simpler
  • These pseudo classes are only supported by input elements
jsx
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} />;
}