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.
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
Import the new CSS file into the component TSX file.
import classes from './Button.module.css'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>
}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.
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;
}Prefer using pseudo classes over classnames for state.
/* Do */
.Button:disabled {
opacity: 0.5;
}
/* Don't */
.ButtonDisabled {
opacity: 0.5;
}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>
)
}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 */
}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);
}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;
}
}Whenever possible, avoid deep nesting as it creates higher specificity selectors. Rely on stylelint to guide how many levels of nesting are acceptable.
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']).
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.
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);
}
}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.
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;
}