Authoring CSS

October 17, 2024 ยท View on GitHub

Primer React uses CSS Modules for styling. CSS Modules allow us to write component scoped CSS while still authoring in a traditional .css file. This guide covers best practices for writing CSS in Primer React.

Getting started

File setup

Create a new .css file in the same directory as the component you are working on. Name the file the same as the component, and add the extension .module.css.

Example: Button.modules.css

Importing CSS

Import the new CSS file into the component TSX file.

import classes from './Button.module.css'

Reference CSS classes

Reference CSS classes in the component TSX file using the classes object.

/* Banner.module.css */

.Banner {
  background-color: var(--banner-bgColor);
}
// Banner.tsx
import classes from './Button.module.css'
import {clsx} from 'clsx'

export function Banner({className}) {
  return <div className={clsx(classes.Banner, className)}>Banner</div>
}

Code styles

CSS classnames

When component classnames are compiled, they receive a prefix of the component name prc-{folder}-{local}- and a suffix of a random hash.

/* Before compilation */
.Container {
  display: inline-block;
}

/* After compilation */
.prc-Button-Container-cBBI {
  display: inline-block;
}

Since classes are prefixed and hashed, the class names themselves can be named generically to represent their intention.

PascalCase

Use PascalCase for classnames. Additional characters like - dashes or _ underscores must be escaped with a \ backslash in TSX for the class name to be recognized, which can be cumbersome.

/* Do */
.ButtonContent {
  display: inline-block;
}

/* Don't */
.button-content {
  display: inline-block;
}

Pseudo elements

Prefer using pseudo classes over classnames for state.

/* Do */
.Button:disabled {
  opacity: 0.5;
}

/* Don't */
.ButtonDisabled {
  opacity: 0.5;
}

clsx and className

Multiple classnames can be referenced on a single node using the clsx utility. This is also useful for providing a className prop alongside the default class name.

The className prop should only be offered on the top-level element of a component. Avoid offering multiple layers of className props to child elements. Consider offering a CSS variable for properties that a consumer may need to customize at the lower levels.

Ensure that other ...props are spread before the className prop to avoid being overridden.

import {clsx} from 'clsx'

export function Button({className, ...props}) {
  return <button {...props} className={clsx(classes.Button, className)} type="button" />
}
// don't offer multiple classNames
export function Button({className, labelClassName}) {
  return (
    <button className={className} type="button">
      <div className={labelClassName}>{label}</div>
    </button>
  )
}

Responsive design

We utilize PostCSS to allow for CSS variables to be used within media queries. The list of available media queries can be found in the @primer/primitives viewport documentation.

To use a viewport variable, write the @media rule as normal and place the variable in between the parentheses.

@media screen and (--viewportRange-regular) {
  /* styles */
}

Component prop variants as data-attributes

When a component has a variant, prefer using a data attribute over a modifier class.

Some common variants include:

  • data-size
  • data-variant
  • data-loading
/* Do */
.Button:where([data-size='small']) {
  height: var(--control-small-size);
}

/* Don't */
.ButtonSmall {
  height: var(--control-small-size);
}

Data attributes can be used as a boolean to represent a true or false state, or as a string to represent a specific value.

/* boolean */

.Button:where([data-loading]) {
  cursor: not-allowed;
}

/* string */

.Button:where([data-size='small']) {
  height: var(--control-small-size);
}

Responsive data attributes

It is common to offer responsive props that allow the consumer to set styling based on the viewport size. This functionality can be extended via data attributes.

import type {ResponsiveValue} from '../hooks/useResponsiveValue'
import {getResponsiveAttributes} from '../internal/utils/getResponsiveAttributes'

// types
type PaddingScale = 'none' | 'condensed' | 'normal' | 'spacious'
type Padding = PaddingScale | ResponsiveValue<PaddingScale>

// prop
type StackProps = {
  padding?: Padding
}

// component
export function Stack({padding = 'normal'}: StackProps) {
  return <div {...getResponsiveAttributes('padding', padding)} />
}
// usage
<Stack padding={{narrow: 'none', regular: 'normal'}} />

By default, we may offer a padding prop. The data attribute for padding might look like data-padding="normal". To make the padding prop responsive, utilize the ResponsiveValue hook alongside the getResponsiveAttributes utility.

// apply the responsive data-attributes using getResponsiveAttributes
export function Stack({padding = 'normal'}: StackProps) {
  return <div {...getResponsiveAttributes('padding', padding)} />
}

By using getResponsiveAttributes, we can reference data attributes in the CSS file based on the prop type offerings.

/* Stack.module.css */
.Stack {
  &:where([data-padding='none']),
  &:where([data-padding-narrow='none']) {
    padding: 0;
  }
}

Specificity and nesting

Whenever possible, avoid deep nesting as it creates higher specificity selectors. Rely on stylelint to guide how many levels of nesting are acceptable.

Using :where to reduce specificity

The :where selector has a specificity of 0, which can be useful for allowing custom overrides. Use the :where selector for component options that utilize data-attributes like &:where([data-size='small']).

CSS variables

Primer primitives

Use CSS variables from @primer/primitives for size, typography, and color values. Certain components also have their own pattern level CSS variables from @primer/primitives that should be used.

Component CSS variables

CSS variables may also be used contextually to set component variants. These CSS variables are defined within the component CSS file.

.Banner {
  background-color: var(--banner-bgColor);

  &:where([data-variant='critical']) {
    --banner-bgColor: var(--bgColor-danger-muted);
  }
}

Fallbacks

Avoid adding fallback values to CSS variables from @primer/primitives. These are added automatically and will be compiled to CSS variables with a fallback value.

Support

Prefer CSS features no newer than Baseline 2022. When using CSS features from Baseline 2023 or newer, provide an appropriate fallback for when the feature is unavailable.

Use @supports to target when a specific piece of functionality is not available

@supports (container-type: inline-size) {
  container: banner / inline-size;
}