---
title: 'Naming Conventions'
source: 'https://academia.sh/en/courses/design-systems/naming-conventions'
course: 'Design Systems'
language: en
updated: '2026-08-19T05:19:54+00:00'
license: 'CC BY-SA 4.0'
---

# Naming Conventions

The derivation rule between design names and code names, the grammar check on names, detecting synonymous-name clashes, and reporting the gap between variant axes and props.

The previous lesson's catalog table rested on an assumption: that when a component is
named, the designer and the developer mean the same thing. Whether the table's
`tooltip` row has a counterpart in the design file, or a part sitting in the design
library never appears in code at all, cannot be read from that table.

The naming convention closes this gap: not an aesthetic exercise, but a
**machine-checkable** mapping between two separate records. Once built, the "in design,
not in code" and "in code, not in design" lists become computable and surface the
system's most common, most silent defect.

## Two Name Sets, One Concept

A component is named in two places. In the design library it carries a hierarchical
name, shown to people and free to contain spaces, capital letters, and punctuation. In
code it carries a module name that appears in the file system and in imports, and it
must be ASCII.

Managing the two name sets **independently** is the system's most common form of decay:
a designer renames a part and the code name stays as it was, or a developer adds a new
component that never gets a counterpart in the design library. Neither produces an
error, because no one has ever asserted an equivalence between the two sets.

The convention is exactly that assertion of equivalence, and it has three parts:

- **Grammar.** The form a design name is written in: how many segments, what
  capitalization.
- **Derivation.** How the code name is computed from the design name — a function,
  since two names "looking alike" cannot be checked, but computing one from the other
  can.
- **Uniqueness.** One name per concept. Two names for the same thing produce two
  components.

## Writing the Derivation Rule

The derivation rule starts with normalization. Typographic characters a design tool's
export can introduce — smart quotes, en and em dashes, ellipses — must convert to their
ASCII equivalents, and that conversion runs before the string is lowercased so every
later step sees a predictable ASCII string. The step is defensive: a name carrying one
of these characters derives a code name the catalog does not expect, and nothing in the
design tool shows the difference.

The program below takes the design library's twenty-one names and the code catalog's
twenty-one names, applies the derivation rule, checks grammar, runs the synonym
dictionary, and finally compares variant axes against props.

```js
// naming.mjs — checking the mapping between design names and code names

// Design library names: "Category/Component" and variant axes.
const DESIGN = [
  { name: "Action/Button",                eksen: ["type", "size", "state"] },
  { name: "Action/Link",                  eksen: ["type", "state"] },
  { name: "Form/Text Field",              eksen: ["size", "state"] },
  { name: "Form/Checkbox",                eksen: ["state", "selected"] },
  { name: "Form/Radio Group",             eksen: ["direction", "state"] },
  { name: "Form/Dropdown",                eksen: ["size", "state"] },
  { name: "Form/Search Field",            eksen: ["size", "state"] },
  { name: "Display/Badge",                eksen: ["tone", "size"] },
  { name: "Container/Card",               eksen: ["elevation", "clickable"] },
  { name: "Navigation/Breadcrumb",        eksen: ["truncation"] },
  { name: "Navigation/Pagination",        eksen: ["size"] },
  { name: "Navigation/Tabs",              eksen: ["direction", "size"] },
  { name: "Feedback/Notification Banner", eksen: ["tone", "dismissible"] },
  { name: "Feedback/Empty State",         eksen: ["size"] },
  { name: "Feedback/Skeleton",            eksen: ["rows"] },
  { name: "Layer/Modal",                  eksen: ["size"] },
  { name: "Layer/Hint Bubble",            eksen: ["direction"] },
  { name: "Container/Accordion",          eksen: ["size"] },
  { name: "Display/Table",                eksen: ["density"] },
  { name: "Card Component",               eksen: ["elevation"] },
  { name: "Form/Input/Number Field",      eksen: ["size"] },
];

// Code catalog names and each component's props.
const CODE = [
  { name: "button",              props: ["type", "size", "state"] },
  { name: "link",                props: ["type", "state"] },
  { name: "text-field",          props: ["size", "state"] },
  { name: "checkbox",            props: ["state", "selected"] },
  { name: "radio-group",         props: ["direction", "state"] },
  { name: "dropdown",            props: ["size", "state", "searchable"] },
  { name: "search-field",        props: ["size", "state"] },
  { name: "badge",               props: ["tone", "size"] },
  { name: "card",                props: ["elevation", "clickable"] },
  { name: "breadcrumb",          props: ["truncation"] },
  { name: "pagination",          props: ["size"] },
  { name: "tabs",                props: ["direction", "size"] },
  { name: "notification-banner", props: ["tone", "dismissible"] },
  { name: "empty-state",         props: ["size"] },
  { name: "skeleton",            props: ["rows"] },
  { name: "modal",               props: ["size", "dismissible"] },
  { name: "tooltip",             props: ["direction"] },
  { name: "accordion",           props: ["size"] },
  { name: "table",               props: ["density"] },
  { name: "number-field",        props: ["size"] },
  { name: "help-text",           props: [] },
];

// Typographic characters a design tool's export can introduce (smart quotes, dashes,
// ellipsis) are normalized to ASCII first, then the string is lowercased. In the
// reverse order, some Unicode characters expand into a multi-character sequence when
// case-folded and the code name breaks without any visible sign in the design tool.
const TRANSLIT = { "’": "'", "‘": "'", "“": '"', "”": '"', "–": "-", "—": "-", "…": "..." };
const normalize = (s) => [...s].map((ch) => TRANSLIT[ch] ?? ch).join("");

// Derivation rule: last segment -> normalize -> lowercase -> space becomes hyphen.
const expectedFromDesign = (designName) => {
  const last = designName.split("/").at(-1);
  return normalize(last).toLowerCase().replace(/\s+/g, "-");
};

// Grammar rule: the name has two segments, each segment in Title Case.
function grammar(designName) {
  const flaws = [];
  const parts = designName.split("/");
  if (parts.length !== 2) flaws.push(`part count ${parts.length}, should be 2`);
  for (const p of parts) {
    for (const word of p.split(/\s+/)) {
      if (word !== word.charAt(0).toUpperCase() + word.slice(1)) {
        flaws.push(`"${word}" is not in Title Case`);
      }
    }
  }
  return flaws;
}

// Synonym dictionary: the one accepted name for a concept and the rejected names.
const SYNONYMS = [
  { accepted: "tooltip", rejected: ["hint-bubble", "explanation-bubble"] },
  { accepted: "modal", rejected: ["dialog", "popup"] },
  { accepted: "notification-banner", rejected: ["alert-box"] },
];

const codeNames = new Set(CODE.map((k) => k.name));

console.log("design name                     expected code name   status");
let matched = 0;
const unmatchedDesign = [];
for (const t of DESIGN) {
  const expected = expectedFromDesign(t.name);
  const found = codeNames.has(expected);
  if (found) matched++; else unmatchedDesign.push(expected);
  console.log(`${t.name.padEnd(32)} ${expected.padEnd(20)} ${found ? "matched" : "NO CODE"}`);
}

const expectedSet = new Set(DESIGN.map((t) => expectedFromDesign(t.name)));
const unmatchedCode = CODE.filter((k) => !expectedSet.has(k.name)).map((k) => k.name);
console.log(`\ncode names with no design counterpart: ${JSON.stringify(unmatchedCode)}`);

console.log("\ngrammar flaws");
for (const t of DESIGN) {
  const flaws = grammar(t.name);
  if (flaws.length) console.log(`${t.name.padEnd(32)} ${flaws.join("; ")}`);
}

console.log("\nsynonym clashes");
const allNames = [...codeNames, ...DESIGN.map((t) => expectedFromDesign(t.name))];
for (const e of SYNONYMS) {
  const present = e.rejected.filter((r) => allNames.includes(r));
  if (present.length) console.log(`${e.accepted.padEnd(20)} used instead: ${JSON.stringify(present)}`);
}

console.log("\nvariant axis vs. prop differences");
const codeMap = new Map(CODE.map((k) => [k.name, k.props]));
for (const t of DESIGN) {
  const props = codeMap.get(expectedFromDesign(t.name));
  if (!props) continue;
  const designOnly = t.eksen.filter((e) => !props.includes(e));
  const codeOnly = props.filter((o) => !t.eksen.includes(o));
  if (designOnly.length === 0 && codeOnly.length === 0) continue;
  console.log(`${t.name.padEnd(32)} design only: ${JSON.stringify(designOnly)}  code only: ${JSON.stringify(codeOnly)}`);
}

const rate = (matched / DESIGN.length) * 100;
console.log(`\nmatched names                : ${matched}/${DESIGN.length} (${rate.toFixed(1)}%)`);
console.log(`no code counterpart          : ${unmatchedCode.length}`);
console.log(`no design counterpart        : ${unmatchedDesign.length}`);
```

```
design name                     expected code name   status
Action/Button                    button               matched
Action/Link                      link                 matched
Form/Text Field                  text-field           matched
Form/Checkbox                    checkbox             matched
Form/Radio Group                 radio-group          matched
Form/Dropdown                    dropdown             matched
Form/Search Field                search-field         matched
Display/Badge                    badge                matched
Container/Card                   card                 matched
Navigation/Breadcrumb            breadcrumb           matched
Navigation/Pagination            pagination           matched
Navigation/Tabs                  tabs                 matched
Feedback/Notification Banner     notification-banner  matched
Feedback/Empty State             empty-state          matched
Feedback/Skeleton                skeleton             matched
Layer/Modal                      modal                matched
Layer/Hint Bubble                hint-bubble          NO CODE
Container/Accordion              accordion            matched
Display/Table                    table                matched
Card Component                   card-component       NO CODE
Form/Input/Number Field          number-field         matched

code names with no design counterpart: ["tooltip","help-text"]

grammar flaws
Card Component                   part count 1, should be 2
Form/Input/Number Field          part count 3, should be 2

synonym clashes
tooltip              used instead: ["hint-bubble"]

variant axis vs. prop differences
Form/Dropdown                    design only: []  code only: ["searchable"]
Layer/Modal                      design only: []  code only: ["dismissible"]

matched names                : 19/21 (90.5%)
no code counterpart          : 2
no design counterpart        : 2
```

## Reading the Report

Nineteen of the twenty-one names match. The remaining four rows — two unmatched design
names and two unmatched code names — represent four kinds of defect, each closed by a
different piece of work.

`Layer/Hint Bubble` and `tooltip` are **the same component with two names**. This is
the most damaging row on the list, because neither side shows a gap: the component
exists on both sides but cannot be linked. The synonym dictionary is the only check
that catches this; unless the rejected names sit next to the accepted one, a checking
program cannot know that two names point to the same concept.

`Card Component` **carries two flaws at once**: it has no category and duplicates the
already-existing `Container/Card` entry. The word "Component" is also outside the
convention — every catalog entry is already a component, so the name saying so again
draws no distinction.

`help-text` **exists in code but not in design**. This does not always mean a defect —
a helper with no visual counterpart existing only in code is ordinary. What the
convention asks is that the row be **explicitly marked**: if the catalog is not
annotated "no design counterpart," the check reports it as a defect every run, and the
report stops being read.

`Form/Input/Number Field` shows an interesting case: it fails the grammar check but
passes the name match, because the derivation rule takes the last segment and the
three-part name still produces the correct code name. This is why the two checks stay
**separate** — the derivation rule can afford to be lenient, but a lenient grammar rule
would scatter categories and make the library harder to search.

## Variant Axis vs. Prop

Naming does not cover the component name alone. In the design library, a component has
axes that produce its variants; in code their counterpart is the props defined in the
Component-Based Interface Development course. Names on both sides must match too,
because the conversation between designer and developer runs through these axis names.

The last block shows two gaps, and both point the same direction: an axis present in
code is missing from design. The `dropdown` component's `searchable` prop and the
`modal` component's `dismissible` prop were added on the code side and never made it
into the design library.

This direction is informative about the cause. An axis that appears in code usually
comes from a product need — some team needed a long list, so a search feature was
added. An axis that appears in design but not in code, by contrast, is a design
decision that was never implemented. The two cases close differently: the first
requires updating the design library, the second requires an item entering the
contribution process.

Axis **values** also need to be part of the convention. Whether the `type` axis takes
"primary" and "secondary" in design and `primary` and `secondary` in code can be
checked; if design says "main action" while code uses `primary`, the conversation
breaks even though the names match.

## Sustaining the Convention

The checking program is not a one-time cleanup tool. Name sets keep changing, and the
gap always accumulates again; two operating decisions determine how well this holds.

The first is whether the check is **blocking**. A check that produces a report but
stops nothing turns into unread output within a few months. Setting a floor for the
match rate and stopping the pipeline below that floor is what turns the report into a
decision.

The second is **recording exceptions**. Legitimate mismatches like `help-text` go into
an exception list, and every entry carries a reason. As that list grows, it becomes
clear the convention itself needs revisiting — the list's length is itself an
indicator.

## Summary

- A component is named in two separate sets; when the two sets are managed
  independently, the gap between them accumulates without producing an error anywhere.
- The convention has three parts: the name's grammar, the derivation rule that computes
  a code name from a design name, and the uniqueness rule that gives one name per
  concept.
- Typographic characters are normalized to ASCII as the **first** step of the
  derivation; a name carrying one of them otherwise derives a code name the catalog
  does not expect.
- A synonym clash shows no gap on either side, so it can only be caught with a synonym
  dictionary; it is the most damaging kind of defect.
- Variant axes and props must match too; the direction of the gap tells you whether the
  work belongs in the design library or in the contribution process.
- Unless the check is tied to a floor and exceptions are recorded with their reasons,
  the report stops being read.

## Next Step

Once names match, it becomes possible to talk about a component, but that is not
enough to **use** it. Which state calls for `dropdown` instead of `search-field`, and
when a `modal` gives way to a separate page instead, cannot be read from names, and
neither can the distinction between `badge` and `notification-banner`. The next lesson
defines the required sections of component documentation, shows why a counter-example
is as necessary as an example, and computes documentation coverage weighted by usage.
