Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSS Modules authoring guide #5037

Merged
merged 18 commits into from
Oct 17, 2024
262 changes: 262 additions & 0 deletions contributor-docs/authoring-css.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# Authoring CSS

Primer React uses [CSS Modules](https://github.com/css-modules/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.

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

### Reference CSS classes

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

```css
/* Banner.module.css */

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

```tsx
// Banner.tsx

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

<div className={classes.Banner}>Banner</>
```

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

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

```css

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

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

#### Pseudo elements

Prefer using pseudo classes over classnames for state.

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

```tsx
import clsx from 'clsx'

export function Button({className, ...props}) {
return <button {...props} className={clsx(classes.Button, className)} />
}
```

```tsx
// don't offer multiple classNames
export function Button({className, labelClassName}) {
return (
<button className={className}>
<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](https://primer.style/foundations/primitives/size#viewport).

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

```css
@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

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

```css

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

```tsx
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](https://github.com/primer/packages/react/src/hooks/useResponsiveValue.ts) hook alongside the [getResponsiveAttributes](https://github.com/primer/react/src/internal/utils/getResponsiveAttributes.ts) utility.

```tsx
// 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.

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

```css
.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

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