---
name: css-modules
description: CSS Modules conventions, Stylelint rules, design tokens (spacing, colors, typography, border-radius), and patterns for the Opentrons monorepo. Use when working with .module.css files or styling React components in app/, components/, protocol-visualization/, protocol-designer/, or other JS packages.
---
# CSS Modules — Opentrons Conventions
## File Naming
File names are **lowercase**, no separators, suffixed with `.module.css`, and match the component name:
```markdown
ComponentName/
├── index.tsx
├── componentname.module.css
└── **tests**/
└── ComponentName.test.tsx
```
Examples: `navbar.module.css`, `labwarebutton.module.css`, `textareafield.module.css`
## Class Naming
Stylelint enforces the pattern `/^[a-z0-9_]+$/` — **snake_case only** (lowercase letters, digits, underscores).
Use a `component_element` or `element_modifier` structure:
```css
/* Base classes */
.button {
}
.slider_container {
}
.crumb_link {
}
/* State/modifier variants */
.button_active {
}
.textarea_error {
}
.title_text_center {
}
.crumb_link_inactive {
}
```
## Importing in Components
```tsx
import styles from './componentname.module.css'
export function MyComponent(): JSX.Element {
return
content
}
```
## Dynamic Styling with `clsx`
Use [clsx](https://github.com/lukeed/clsx) for combining or conditionally applying classes:
```tsx
import clsx from 'clsx'
import styles from './componentname.module.css'
export function MyButton({ isActive, isError }: Props): JSX.Element {
const className = clsx(styles.button, {
[styles.button_active]: isActive,
[styles.button_error]: isError,
})
return
}
```
## Design Tokens (CSS Custom Properties)
All tokens are defined in `components/src/styles/global.css`. **Always use these variables** instead of hard-coding values for colors, spacing, typography, and border-radius.
### Spacing
| Variable | Value |
| --------------- | --------------- |
| `--spacing-2` | 0.125rem (2px) |
| `--spacing-4` | 0.25rem (4px) |
| `--spacing-6` | 0.375rem (6px) |
| `--spacing-8` | 0.5rem (8px) |
| `--spacing-10` | 0.625rem (10px) |
| `--spacing-12` | 0.75rem (12px) |
| `--spacing-16` | 1rem (16px) |
| `--spacing-20` | 1.25rem (20px) |
| `--spacing-24` | 1.5rem (24px) |
| `--spacing-32` | 2rem (32px) |
| `--spacing-40` | 2.5rem (40px) |
| `--spacing-44` | 2.75rem (44px) |
| `--spacing-48` | 3rem (48px) |
| `--spacing-60` | 3.75rem (60px) |
| `--spacing-68` | 4.25rem (68px) |
| `--spacing-80` | 5rem (80px) |
| `--spacing-120` | 7.5rem (120px) |
### Colors
**Core:** `--white` (#fff), `--black-90` (#16212d), `--black-80` (#24313f), `--black-70` (#39495b)
**Grey:** `--grey-10` through `--grey-60` (10, 20, 30, 35, 40, 50, 55, 60)
**Blue:** `--blue-10` through `--blue-60` (10, 20, 30, 35, 40, 50, 55, 60)
**Purple:** `--purple-20` through `--purple-60` (20, 30, 35, 40, 50, 55, 60)
**Green:** `--green-20` through `--green-60` (20, 30, 35, 40, 50, 60)
**Red:** `--red-20` through `--red-60` (20, 30, 35, 40, 50, 55, 60)
**Yellow:** `--yellow-20` through `--yellow-60` (20, 30, 35, 40, 50, 60)
**Flex brand:** `--flex-20` through `--flex-60` (20, 30, 35, 40, 50, 55, 60)
**Semi-transparent white:** `--transparent-white-20` through `--transparent-white-80` (20, 30, 50, 80)
**Semi-transparent black:** `--transparent-black-10` through `--transparent-black-80` (10, 20, 30, 40, 50, 60, 80)
**transparent:** `--transparent`
> Some colors (green, purple) have touchscreen variants that activate when `components/src/styles/global.css` applies the `.enable_touchscreen_colors` class. The default values are non-touchscreen.
### Border Radius
`--border-radius-2`, `--border-radius-4`, `--border-radius-8`, `--border-radius-12`, `--border-radius-16`, `--border-radius-40`, `--border-radius-full` (pill/circle)
### Typography
**Font size:** `--font-size-9`, `--font-size-10`, `--font-size-11`, `--font-size-12`, `--font-size-13`, `--font-size-14`, `--font-size-15`, `--font-size-16`, `--font-size-18`, `--font-size-19`, `--font-size-20`, `--font-size-22`, `--font-size-23`, `--font-size-24`, `--font-size-26`, `--font-size-28`, `--font-size-32`, `--font-size-38`, `--font-size-39`, `--font-size-80`
**Font weight:** `--font-weight-light` (300), `--font-weight-regular` (400), `--font-weight-semi-bold` (600), `--font-weight-bold` (700)
**Line height:** `--line-height-12`, `--line-height-16`, `--line-height-18`, `--line-height-20`, `--line-height-24`, `--line-height-28`, `--line-height-36`, `--line-height-42`, `--line-height-48`, `--line-height-96`
## Where to Use Tokens vs Explicit Values
| Property | Use token? | Example |
| -------------------------------------------- | --------------------------- | ------------------------------ |
| `color`, `background-color`, `border-color` | **Yes** | `var(--blue-50)` |
| `padding`, `margin`, `gap` | **Yes** | `var(--spacing-16)` |
| `border-radius` | **Yes** | `var(--border-radius-8)` |
| `font-size` | **Yes** | `var(--font-size-p)` |
| `font-weight` | **Yes** | `var(--font-weight-semi-bold)` |
| `line-height` | **Yes** | `var(--line-height-20)` |
| `width`, `height`, `max-width`, `min-height` | **No** — use explicit `rem` | `15rem`, `100vh` |
| `box-shadow` | **No** — use `px` | `0 0 0 2px var(--blue-50)` |
## Unit Rules
- **rem** for all dimensions — divide design-spec px by 16 (`240px / 16 = 15rem`)
- **px** only for `box-shadow` values and `
` tag dimensions
- Prefer `padding` over `margin` for wrapper and container elements
- Avoid complex `calc()` expressions
## State & Pseudo-Class Handling
```css
.button:focus {
outline: none;
box-shadow: 0 0 0 2px var(--blue-50);
}
.button:hover:not(:disabled) {
background-color: var(--grey-10);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.input:focus {
border-color: var(--blue-50);
}
```
Always guard hover styles with `:not(:disabled)` on interactive elements.
## CSS Modules Features
### Composition
Stylelint allows the `composes` property for class composition:
```css
.base_button {
padding: var(--spacing-8) var(--spacing-16);
border-radius: var(--border-radius-4);
}
.primary_button {
composes: base_button;
background-color: var(--blue-50);
color: var(--white);
}
```
### Global Selectors
Use `:global()` to target non-module classes (e.g., from React Router or third-party libraries):
```css
.nav_link:global(.active) {
color: var(--blue-50);
}
```
## Stylelint Configuration
The repo uses `stylelint-config-standard` + `stylelint-config-idiomatic-order` (property ordering). Key enforced rules:
- Class selector pattern: `/^[a-z0-9_]+$/`
- Max 4 decimal places for numbers (except dimension properties like `width`, `height`, `flex`)
- Standard CSS property ordering (idiomatic order)
- `@value` at-rule is still allowed but deprecated — avoid in new code
### Tips: Common Stylelint Fixes
**Zero lengths must have no unit** (`length-zero-no-unit`):
```css
/* ✖ Unexpected unit */
box-shadow: 0px 3px 6px rgb(0 0 0 / 23%);
/* ✔ Use bare 0 — no px/rem/etc. */
box-shadow: 0 3px 6px rgb(0 0 0 / 23%);
```
**Use modern `rgb` notation** (`color-function-alias-notation`, `color-function-notation`):
```css
/* ✖ rgba + comma-separated args are legacy */
box-shadow: 0 3px 6px rgba(0, 0, 0, 23%);
/* ✔ Prefer rgb with space-separated channels and / alpha */
box-shadow: 0 3px 6px rgb(0 0 0 / 23%);
```
## Linting and Formatting
```bash
# Lint all CSS
make lint-css
# Auto-fix all CSS
make format-css
# Lint a single file
pnpm stylelint path/to/componentname.module.css
# Auto-fix a single file
pnpm stylelint path/to/componentname.module.css --fix
```
Always run `make lint-css` and fix any issues before committing.
## Legacy Patterns (Do Not Introduce)
- `components/src/styles/borders.module.css` — old border tokens (`--bd-radius-*`, `--bd-width-*`, `--shadow-lvl-*`). Use `global.css` tokens instead.
- `components/src/styles/typography.module.css` — old typography tokens (`--fs-*`, `--fw-*`, `--lh-*`). Use `global.css` tokens instead.
- `styled-components` — the codebase is migrating away. Always use CSS Modules for new styles.
- `@value` declarations — still parsed by stylelint but deprecated. Use CSS custom properties.