Skip to main content

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:

DistributionClass namesWho uses it
As-is CSS ModulesHashed by the app bundlerReact apps
Bundled all.cssStable, readableCDN, 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.

SituationLocal classall.css
Rootrootyds-Button
ElementTextWrapperyds-Button__TextWrapper
Boolean prop when trueisDisabledyds-Button__isDisabled
Named prop valuevariant_primaryyds-Button__variant_primary
Element + propTextWrapper_size_xsyds-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.

PropClass
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: root is <select>, not the fieldset. The fieldset is Fieldset; the chrome around the select is Wrapper.
  • CheckboxGroup: the group component’s outermost node is root, not Container. Container is 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​

LocalMeaning
WrapperChrome around a single control, not the root. Example: a div around <select>.
ContainerChrome around several similar elements, inside another component. Example: Checkbox group with multiple Checkbox components.
IconWrapperShell around an SVG or icon component.
SpinnerWrapperShell around <Spinner>.
[Slot]WrapperOther inner shell that may hold another component (TextWrapper, LabelWrapper).

Form​

LocalMeaning
Fieldset<fieldset>
Legend<legend> — not Label
Label<label> or the node targeted by aria-labelledby
DescriptionLonger helper text
ErrorMessageError text
Input<input> when it is not root
Option<option> — Item is an <li> or selectable repeating row
UnitSuffix such as € or %
CounterCharacter counter

If Label is a separate FormElementLabel component, its classes are yds-FormElementLabel, not yds-Select__Label.

Structure​

LocalMeaning
HeaderTop region
FooterBottom region (chrome, not the button list)
TitleHeading text
ContentMain slot
ActionsAction slot. Footer is the region; Actions is its contents.

Overlay and disclosure​

LocalMeaning
Dialog<dialog>, often root
HandleDrag handle
CloseButtonClose control — allowed because the slot is universal
TriggerOpening control when it is not Title
PanelRevealed region (tab / accordion) when you need to distinguish it from Content
MenuPopup list, not necessarily a <ul>

Collections​

LocalMeaning
List<ul> / <ol>
Item<li> or repeating row
SeparatorDivider
Nav<nav> when it is not root

Meters​

LocalMeaning
TrackTrack (progress, slider)
FillFill of the track
ThumbSlider knob, if it is its own node
IndicatorOne-of-N mark (page dots). Not the progress fill.

Other recurring slots​

LocalMeaning
IconThe icon itself, usually SVG. The shell is IconWrapper.
BadgeDecoration on another component
UnderlayHit area behind a control
ButtonNested button that is not the root. Use SubmitButton when the slot is fixed.

Not element names​

  • VisuallyHidden — a state (isHidden) or a shared utility, not yds-Select__VisuallyHidden.
  • Backdrop — only if it is a real DOM node. ::backdrop is 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:

  1. Put the style where the markup is. ButtonGroup applies Button’s own inGroup class from Button.module.css onto the Button root (via className or appearanceClassName). Button then styles .inGroup .TextWrapper. That is not a public React prop.
  2. 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 / _modifier prefixes in the module (they would triple-underscore after generateScopedName)
  • Combinatorial classes that encode several props (grid_banner_icon_actions_close) — use separate modifiers. Notification layout templates in BaseNotification are a known exception: they are mutually exclusive grid-template-areas maps, not independent props.
  • Using the component name as the root class (checkboxGroup, spinner) — use root
  • Documenting a parallel kebab namespace for all.css

See also​