Lesson 18 / 23
Naming Methodologies
The block-element-modifier approach; keeping specificity flat, a name announcing ownership, the separation between modifier and state, and the pattern being checkable.
Contents
Three stylesheets have accumulated in this course. Visual declarations in station.css,
layout in layout.css, motion in motion.css. Class names have multiplied too: .card,
.heading, .wide, .summary, .indicator.
None of these names say who they belong to. Is .heading the measurement card’s heading, or
the aside’s? Is it safe to use the same name when a new section is added to the page? What
else breaks when a rule is deleted?
This topic ties the answer to these questions to an architecture. This lesson takes the first step: the names themselves.
What Naming Solves
As a stylesheet grows, three problems appear.
Ownership. If which component a class name belongs to cannot be read from the name itself, no one can change that name with confidence. The place to make the change cannot be found.
Specificity creep. One more class gets added to a selector to override a declaration; then
one more to override that. The triple defined in the Cascade and Specificity lesson starts
growing, and !important eventually gets written.
Deletability. If which part of the document a rule matches is unknown, that rule cannot be deleted. The stylesheet only grows.
A naming methodology takes on all three with a single decision: a name is derived not from the element’s place in the document, but from the component it belongs to.
Block, Element, Modifier
The common methodology splits a name into three parts.
A block is a component that is meaningful and portable on its own. Wherever it is placed on the page, it is the same thing: the measurement card, the navigation bar, the measurement filter.
An element is part of the block and has no meaning without it: the card’s heading, the
card’s value, the card’s unit. The name starts with the block’s name and is separated by two
underscores: measurement-card__heading.
A modifier is a variant of the block or the element: a wide card, a warning card, a short
heading. It is separated by two hyphens: measurement-card--wide.
One term distinction is needed: here, element means a part of a block. Nodes in the HTML
document are called elements in this and earlier courses too. The two words name different
things; measurement-card__heading is an element name and can be written on an h3 element.
Keeping Specificity Flat
The methodology’s critical consequence is that every rule has a single class selector. The program below computes selectors’ specificity triples and compares the distribution of two notations.
// naming.mjs — checking selector specificity and name patterns // Specificity triple (a, b, c): a=id, b=class/attribute/pseudo-class, c=type/pseudo-element // :where(...) content adds no specificity; the most specific selector inside :is(...) counts. function specificity(selector) { let s = selector; let extra = [0, 0, 0]; s = s.replace(/:where\([^()]*\)/g, " "); // no contribution s = s.replace(/:is\(([^()]*)\)/g, (_, inner) => { // most specific branch counts const branches = inner.split(",").map((d) => specificity(d.trim())); branches.sort((x, y) => y[0] - x[0] || y[1] - x[1] || y[2] - x[2]); extra = extra.map((v, i) => v + branches[0][i]); return " "; }); const a = (s.match(/#[\w-]+/g) || []).length; const b = (s.match(/\.[\w-]+/g) || []).length + (s.match(/\[[^\]]+\]/g) || []).length + (s.match(/:(?!:)[\w-]+(\([^()]*\))?/g) || []).length; const c = (s.match(/::[\w-]+/g) || []).length + (s.replace(/::[\w-]+/g, " ").replace(/:(?!:)[\w-]+(\([^()]*\))?/g, " ") .match(/(^|[\s>+~(])([a-zA-Z][\w-]*)/g) || []).length; return [a + extra[0], b + extra[1], c + extra[2]]; } const SELECTORS = [ "#content .measurements-section ul li a.active", ".station-layout .measurements-section .measurement-cards .card .heading", "section.measurements-section > ul > li", ".measurement-card__heading", ".measurement-card--warning", ".measurement-card:hover", ".measurement-card[data-status='missing']", ":where(.measurement-cards) .measurement-card__heading", ":is(h2, .heading).measurement-card__heading", ]; console.log("--- selector specificities ---"); console.log("selector".padEnd(58) + "specificity".padStart(12) + " depth"); for (const s of SELECTORS) { const [a, b, c] = specificity(s); const depth = s.split(/[\s>+~]+/).filter(Boolean).length; console.log(s.padEnd(58) + `(${a}, ${b}, ${c})`.padStart(12) + String(depth).padStart(10)); } // --- checking the name pattern: block__element--modifier --- const PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(__[a-z][a-z0-9]*(-[a-z0-9]+)*)?(--[a-z][a-z0-9]*(-[a-z0-9]+)*)?$/; const NAMES = [ "measurement-card", "measurement-card__heading", "measurement-card--wide", "measurement-card__value--missing", "measurement-card__body__row", "MeasurementCard", "measurement_card", "card--", "measurement-card__heading--short", ]; console.log("\n--- name pattern check ---"); for (const name of NAMES) { const parts = name.split("__"); const multiElement = parts.length > 2; const valid = PATTERN.test(name) && !multiElement; const reason = multiElement ? "nested element name" : !PATTERN.test(name) ? "off-pattern spelling" : "-"; console.log(` ${name.padEnd(34)} ${(valid ? "valid" : "invalid").padEnd(9)} ${reason}`); } // --- the specificity distribution the same component's rules produce in two notations --- console.log("\n--- specificity of all of one component's rules ---"); const NESTED = [ ".measurement-cards .card", ".measurement-cards .card .heading", ".measurement-cards .card .heading span", ".measurement-cards .card.wide .heading", ]; const FLAT = [".measurement-card", ".measurement-card__heading", ".measurement-card__unit", ".measurement-card--wide .measurement-card__heading"]; const distribution = (list) => list.map((s) => specificity(s).join(".")); console.log(" nested notation:", distribution(NESTED).join(" ")); console.log(" flat notation :", distribution(FLAT).join(" "));
--- selector specificities --- selector specificity depth #content .measurements-section ul li a.active (1, 2, 3) 5 .station-layout .measurements-section .measurement-cards .card .heading (0, 5, 0) 5 section.measurements-section > ul > li (0, 1, 3) 3 .measurement-card__heading (0, 1, 0) 1 .measurement-card--warning (0, 1, 0) 1 .measurement-card:hover (0, 2, 0) 1 .measurement-card[data-status='missing'] (0, 2, 0) 1 :where(.measurement-cards) .measurement-card__heading (0, 1, 0) 2 :is(h2, .heading).measurement-card__heading (0, 2, 0) 2 --- name pattern check --- measurement-card valid - measurement-card__heading valid - measurement-card--wide valid - measurement-card__value--missing valid - measurement-card__body__row invalid nested element name MeasurementCard invalid off-pattern spelling measurement_card invalid off-pattern spelling card-- invalid off-pattern spelling measurement-card__heading--short valid - --- specificity of all of one component's rules --- nested notation: 0.2.0 0.3.0 0.3.1 0.4.0 flat notation : 0.1.0 0.1.0 0.1.0 0.2.0
The last block fits the methodology’s entire justification into two lines. In the nested notation, the same component’s four rules sit at four different specificities; overriding one with another requires knowing which selector is how specific. In the flat notation, three rules sit at the same specificity, and only write order decides the ordering between them.
Deciding between rules at the same specificity is easier than deciding between rules at different specificities: whichever is written later wins, no other computation is needed.
The table above shows two more tools. A selector wrapped in :where() adds nothing to
specificity — this is the way to write a container without growing the triple. :is(), on the
other hand, counts the most specific branch inside it, and so does grow the triple.
The Pattern Being Checkable
A naming methodology stays practical only if it can be checked by machine. The second block tests nine names against a single regular expression and rejects four of them.
Three of the rejected ones are spelling-rule mistakes: an uppercase letter, a misused
underscore separator, an empty modifier. The fourth is more interesting:
measurement-card__body__row follows the spelling rules but contains two element names.
This prohibition is deliberate. A nested element name embeds the document’s tree structure into the name; when the heading is pulled out of the card’s body, the name becomes wrong. A block is flat: every element belongs directly to the block, and the tree relation between them is not written into the name.
The same check also finds names that exist in the stylesheet but are never used in the document. Since the name pattern is fixed, comparing the two sets is a matter of string matching.
Modifier or State
A modifier is a variant of the component known at design time: a wide card, a warning card. It is written when the document is produced and never changes over its lifetime.
State, on the other hand, changes at runtime: open, selected, loading, errored. If the two are written with the same mechanism, which class a script is allowed to change becomes unclear.
The way to keep the distinction is to carry state through a separate mechanism. There are two
options, and both work with either data- attributes or aria- attributes:
.measurement-card[data-status="missing"] { border-color: var(--warning); } .measurement-card[aria-expanded="true"] .measurement-card__body { display: block; }
The second is preferred. If the state is already announced to the accessibility tree, style reads that same announcement; a single source does two jobs. Writing a separate state class means keeping the same information in two places, and the two can drift apart.
Layer Prefixes
Naming methodologies organize the inside of a component; to organize the space between components, adding a layer prefix to the name is common. Prefixes vary by approach, but the function is the same: the name tells which layer a rule comes from.
The layers usually separated out are: layout rules that build the page skeleton, portable
components, single-declaration utility classes, and runtime states. This separation can also
be built with @layer, in which case there is no need to write it into the name — cascade
layers already announce a rule’s order too. Layers are taken up in this topic’s sixth lesson.
Renaming the Station Page
/* components/measurement-card.css — step 1: all of the component's rules in one file */ .measurement-card { display: grid; gap: var(--spacing-0); padding: var(--spacing-1); border: 1px solid var(--line); transition: transform 240ms ease-out; } .measurement-card__heading { font-size: 1rem; color: var(--text-muted); } .measurement-card__value { font-size: 2rem; font-variant-numeric: tabular-nums; } .measurement-card__unit { color: var(--text-muted); } .measurement-card--wide { grid-column: span 2; } .measurement-card--summary { grid-column: 1 / -1; } .measurement-card[data-status="missing"] .measurement-card__value { color: var(--warning); } .measurement-card:hover, .measurement-card:focus-within { transform: translateY(-8px) scale(1.04); }
The component’s visual, layout, and motion declarations are gathered into one file. Understanding how a card looked when split across three files required reading three files; now there is one file per component, and the file name can be read from the class name’s root.
The container’s grid definition — how the cards line up side by side — is not in this file.
The component knows its own inside, not its outside; outer layout stays in the
.measurement-cards rule.
Summary
- A naming methodology derives a name from the component it belongs to, not from the element’s place in the document; it takes on ownership, specificity creep, and deletability together.
- A block is a portable component, an element is a part belonging to the block, a modifier is a variant; element names do not nest, because tree structure is not written into the name.
- In flat notation, all of a component’s rules sit at the same specificity and only write order decides between them; in nested notation, every rule lands at a different specificity.
:where()adds nothing to specificity,:is()counts the most specific branch inside it; both are tools for keeping the triple under control.- A design-time variant is written with a modifier, a runtime state with an attribute; if accessibility attributes exist, style reads them.
Next Step
Every rule in the component file refers to names like var(--line), var(--warning),
var(--spacing-1). These names were defined in the Visual Presentation with CSS course and
kept on the root element. This single-centered structure is not enough once a component needs
to set its own scale, a section needs to carry its own theme, or a modifier needs to change
only a few values. The next lesson takes up custom properties as an architectural tool.
To keep your progress and take notes, Log in
My notes
Log in to take notes.