CSS class naming conventions
There are only two hard things in Computer Science: cache invalidation and naming things.
— Phil Karlton
This page describes how YDS names classes in CSS Modules and in the bundled all.css. Follow this when you add or change styles in @yleisradio/yds-components-react.
There are two distributions from the same .module.css source:
| Distribution | Class names | Who uses it |
|---|---|---|
| As-is CSS Modules | Hashed by the app bundler | React apps |
Bundled all.css | Stable, readable | CDN, prototypes, HTML without React |
Local names in the module are the contract. all.css only prefixes them.
Grammar
-- modifiers are not used: they are awkward as JavaScript identifiers (styles['variant--primary']). The separator is a single underscore.
yds-[Component] is added only when bundling all.css. Do not write that prefix in .module.css.
| Situation | Local class | all.css |
|---|---|---|
| Root | root | yds-Button |
| Element | TextWrapper | yds-Button__TextWrapper |
Boolean prop when true | isDisabled | yds-Button__isDisabled |
| Named prop value | variant_primary | yds-Button__variant_primary |
| Element + prop | TextWrapper_size_xs | yds-Button__TextWrapper_size_xs |
Elements are PascalCase (initial capital). Props are camelCase (initial lowercase). A consumer — or a small helper — can tell them apart without a lookup table:
- starts with
A–Z→ element; anything after the first_is a modifier - otherwise → prop on the root
In all.css:
yds-Button
yds-Button__variant_primary
yds-Button__isDisabled
yds-Button__TextWrapper
yds-Button__TextWrapper_size_xs
generateScopedName maps root to yds-[Component] with no __root suffix. Every other local name becomes yds-[Component]__[local].
Do not camelCase a prop into the element name. Title_size_md, never titleSize_md.
Building names from props
For all.css consumers the public React prop names are the vocabulary.
| Prop | Class |
|---|---|
variant="primary" | variant_primary |
size="md" | size_md |
isDisabled={true} | isDisabled |
disabled (html attribute) | root[disabled] (in addition to a possible prop class) |
Boolean classes are omitted when the value is false. Derived internals (hasIconBefore) should be avoided: prefer a sibling selector (.IconWrapper + .TextWrapper) so HTML consumers do not have to reconstruct React-only state.
A helper is optional DX, not a second source of truth. If you add one, generate it from the same local names.
Root
root is the public node: the element that receives className, ref, and native props.
- Select:
rootis<select>, not the fieldset. The fieldset isFieldset; the chrome around the select isWrapper. - CheckboxGroup: the group component’s outermost node is
root, notContainer.Containeris only a grouping slot inside another component.
Every component module should define .root.
Element vocabulary
Use these names when the slot matches. Component-specific PascalCase names are allowed when nothing below fits. Do not invent synonyms (Box, Inner, Holder, Shell).
Shells
| Local | Meaning |
|---|---|
Wrapper | Chrome around a single control, not the root. Example: a div around <select>. |
Container | Chrome around several similar elements, inside another component. Example: Checkbox group with multiple Checkbox components. |
IconWrapper | Shell around an SVG or icon component. |
SpinnerWrapper | Shell around <Spinner>. |
[Slot]Wrapper | Other inner shell that may hold another component (TextWrapper, LabelWrapper). |
Form
| Local | Meaning |
|---|---|
Fieldset | <fieldset> |
Legend | <legend> — not Label |
Label | <label> or the node targeted by aria-labelledby |
Description | Longer helper text |
ErrorMessage | Error text |
Input | <input> when it is not root |
Option | <option> — Item is an <li> or selectable repeating row |
Unit | Suffix such as € or % |
Counter | Character counter |
If Label is a separate FormElementLabel component, its classes are yds-FormElementLabel, not yds-Select__Label.
Structure
| Local | Meaning |
|---|---|
Header | Top region |
Footer | Bottom region (chrome, not the button list) |
Title | Heading text |
Content | Main slot |
Actions | Action slot. Footer is the region; Actions is its contents. |
Overlay and disclosure
| Local | Meaning |
|---|---|
Dialog | <dialog>, often root |
Handle | Drag handle |
CloseButton | Close control — allowed because the slot is universal |
Trigger | Opening control when it is not Title |
Panel | Revealed region (tab / accordion) when you need to distinguish it from Content |
Menu | Popup list, not necessarily a <ul> |
Collections
| Local | Meaning |
|---|---|
List | <ul> / <ol> |
Item | <li> or repeating row |
Separator | Divider |
Nav | <nav> when it is not root |
Meters
| Local | Meaning |
|---|---|
Track | Track (progress, slider) |
Fill | Fill of the track |
Thumb | Slider knob, if it is its own node |
Indicator | One-of-N mark (page dots). Not the progress fill. |
Other recurring slots
| Local | Meaning |
|---|---|
Icon | The icon itself, usually SVG. The shell is IconWrapper. |
Badge | Decoration on another component |
Underlay | Hit area behind a control |
Button | Nested button that is not the root. Use SubmitButton when the slot is fixed. |
Not element names
- VisuallyHidden — a state (
isHidden) or a shared utility, notyds-Select__VisuallyHidden. - Backdrop — only if it is a real DOM node.
::backdropis not a class. - Spinner / Select / Checkbox as a child class — that is another component, not a local name on the parent.
Authoring
/* Button.module.css */
.root {
/* … */
}
.variant_primary {
background-color: var(--yds-color-action-primary);
}
.isDisabled {
cursor: not-allowed;
}
.TextWrapper {
display: block;
}
.TextWrapper_size_xs {
padding-left: 0.25rem;
}
.IconWrapper {
display: inline-block;
}
className={cx(
styles.root,
styles[`variant_${variant}`],
isDisabled && styles.isDisabled
)}
Shared primitives live in src/internal/styles/ (typography.module.css, focus.module.css). Compose those; do not copy token values.
Cross-component styling
Do not add a second, kebab-case global BEM name (yds-Button__TextWrapper) next to the module class. This risks getting all.css to spread in micro-frontend architecture, where an app uses no hashes.
A parent must not select another component’s hashed internals. Instead:
- Put the style where the markup is. ButtonGroup applies Button’s own
inGroupclass fromButton.module.cssonto the Button root (viaclassNameorappearanceClassName). Button then styles.inGroup .TextWrapper. That is not a public React prop. - Pass color through a custom property. Parents set
--yds-spinner-stroke-color; Spinner’s wheel reads it with a token fallback. No:global(.yds-spinner)pierce.
Theme and focus utilities are a different contract: yds-theme-light, yds-theme-dark, yds-focus. They are not component slots.
What to avoid
- kebab-case locals or
--modifiers __element/_modifierprefixes in the module (they would triple-underscore aftergenerateScopedName)- Combinatorial classes that encode several props (
grid_banner_icon_actions_close) — use separate modifiers. Notification layout templates inBaseNotificationare a known exception: they are mutually exclusivegrid-template-areasmaps, not independent props. - Using the component name as the root class (
checkboxGroup,spinner) — useroot - Documenting a parallel kebab namespace for
all.css