CEL (Context · Element · Layer) is HUX’s front-end naming and architecture methodology. It builds on the structural logic of BEM — Block, Element, Modifier — and extends it into the era of tokenised design systems and AI-assisted development.
Where BEM was built for static stylesheets authored entirely by hand, CEL is built for systems where design decisions are encoded as tokens, passed through semantic layers, and consumed by both humans and machines. The class name is a contract. The token is the substance.
CEL answers a question BEM cannot: how does the system communicate design intent to an LLM, a code generator, or a component scaffolding tool without losing fidelity?
BEM solved a real problem: it gave teams a shared naming convention that reduced specificity conflicts and made CSS scalable. That contribution holds.
But BEM was built before design tokens existed as a concept. It has no native understanding of the relationship between a class name and the token that gives it its visual properties. A BEM class like .card--featured tells a developer what state a component is in. It tells a machine nothing about what that state looks like, why it exists, or what it controls.
CEL adds that layer. The class name identifies the scope. The token delivers the value. A machine-readable attribute on the element surfaces the relationship so AI tooling can reason about it without reverse-engineering the stylesheet.
CEL uses the same double-underscore and double-hyphen separators as BEM. If you know BEM, the reading is familiar. The meaning is extended.
context__element--layer
card → context only
card__title → context + element
card__title--featured → context + element + layer
card--dark → context + layer (no element)
The Context is the named component scope. It is the outermost boundary of a self-contained UI unit. A Context maps directly to a design token namespace and, in most cases, to a component in the design system.
The Context class appears on the root element of the component. Every element and layer within the component is scoped to it.
<section class="case-study">
<h2 class="case-study__heading">Project name</h2>
<p class="case-study__body">Description text</p>
</section>
An Element is a named part inside a Context. It is not a generic HTML element — it is a meaningful structural piece of the component with its own token mapping.
Elements are named for their role, not their tag. Use heading, not h2. Use label, not span. Use body, not p. This keeps the class name stable when markup changes.
card__image
card__heading
card__tag
card__body
card__cta
A Layer is a state, variant, or condition applied to a Context or Element. Layers modify the visual or functional presentation without changing the structural role of the element.
A Layer always references a token or a set of tokens. If the layer has no corresponding token, it should not exist as a Layer — it belongs in a separate Context.
card--featured → a variant token namespace (--card-featured-bg)
card__heading--dark → a conditional text token (--card-heading-dark-color)
btn--loading → an interactive state (--btn-loading-opacity)
nav__item--active → a selection state (--nav-item-active-border)
Every CEL class has a corresponding position in the token stack. This is non-negotiable. A class without a token is a one-off. A token without a class is inaccessible. The two must be bound.
HUX’s token stack runs in three tiers:
Primitive
Raw, unnamed values. No semantic meaning. Never used directly in components. e.g. --color-blue-500, --space-4, --radius-2
Semantic
Named for intent, not appearance. Maps a primitive to a role. e.g. --color-action, --space-component-gap, --radius-card
Component
Scoped to a specific Context. Consumes a semantic token. e.g. --btn-bg, --card-heading-color, --nav-item-border
The primitive layer is invisible to authors and machines at the component level. Semantic tokens provide the shared language across components. Component tokens are the implementation detail — the exact value a class resolves to.
/* Primitive */
-color-blue-500: #1D4ED8;
/* Semantic */
-color-action: var(--color-blue-500);
/* Component */
-btn-bg: var(--color-action);
/* Class */
.btn { background: var(--btn-bg); }
Each Layer must extend the component token namespace with a suffixed variant. This makes the layer’s token contract explicit and machine-discoverable.
/* Component base */
-card-bg: var(--color-surface);
-card-heading-color: var(--color-text-primary);
/* Layer: featured */
-card-featured-bg: var(--color-surface-accent);
-card-featured-heading-color: var(--color-text-inverse);
.card { background: var(--card-bg); }
.card--featured { background: var(--card-featured-bg); }
.card--featured .card__heading { color: var(--card-featured-heading-color); }
CEL’s class names are designed to be human-readable. Its data attributes are designed to be machine-readable. The two serve different consumers.
Every component root in a CEL system carries a minimal set of data attributes that expose the design contract to tooling, LLMs, and CI validators:
data-cel-context
The component name. Matches the CSS class prefix.
data-cel-layer
The active variant or state layer, if any.
data-cel-token
The root-level component token namespace.
data-cel-maturity
The maturity stage of this component in the system lifecycle.
<div
class="card card--featured"
data-cel-context="card"
data-cel-layer="featured"
data-cel-token="--card"
data-cel-maturity="stable"
These attributes allow an LLM or a linting tool to traverse the DOM and reconstruct the design intent of any element without parsing CSS. Given data-cel-token="--card", a machine can query the token map and understand exactly what values control the appearance of this component and why.
The data-cel-maturity attribute reflects the HUX maturity journey. It signals to both humans and machines how stable this component is, how much it should be trusted, and whether it is ready for production use.
draft
Component exists but has no stable token contract. Do not consume in production.
review
Token contract is proposed. Under review for semantic correctness.
stable
Token contract is settled. Component is production-ready.
deprecated
Component is marked for removal. Machine tooling should flag usage.
CEL systems follow a predictable directory structure that mirrors the token and class architecture. Contexts map to files. Layers live inside their parent Context file. Tokens live in a parallel directory that mirrors the component tree.
tokens/
primitives/
color.css
space.css
radius.css
semantic/color.css
space.css
components/card.css
btn.cssnav.css
components/
card/
card.html
card.css
card.js (optional)
btn/btn.htmlbtn.cssnav/nav.htmlnav.css
Token files use CSS custom properties scoped to :root. Component token files import from semantic, which import from primitives. The cascade is always unidirectional: primitives → semantic → component. A component token never references another component token.
/* tokens/components/card.css
*/@import "../semantic/color.css";
@import "../semantic/space.css";
:root {
--card-bg: var(--color-surface);
--card-border: var(--color-border-subtle);
--card-radius: var(--radius-card);
--card-padding: var(--space-component-gap);
--card-heading-color: var(--color-text-primary);
--card-body-color: var(--color-text-secondary);
/* Layer: featured */
--card-featured-bg: var(--color-surface-accent);
--card-featured-border: var(--color-border-accent);
--card-featured-heading-color: var(--color-text-inverse);
}
• Context names are lowercase, single words where possible. Compound contexts use a hyphen: case-study, nav-primary.
• Element names describe role, not tag or appearance: heading not h2, label not span, media not img.
• Layer names describe the condition or variant in plain language: featured, loading, active, dark, collapsed.
• No more than three parts in a class name. If you need four, you have a new Context.
• Every class that changes visual output must reference a component token. No hardcoded values in component CSS.
• Layer tokens must be suffixed with the layer name: --card-featured-bg not --card-bg-featured.
• Semantic tokens must never encode a primitive value directly. If you find yourself writing --color-action: #1D4ED8, the value belongs in a primitive.
• Component tokens must never skip the semantic layer. --btn-bg: var(--color-blue-500) is invalid. It must be --btn-bg: var(--color-action).
• All component root elements carry the four data-cel-* attributes.
• data-cel-layer is updated dynamically by JavaScript when state changes. It is never hardcoded as a variant-only attribute in markup.
• data-cel-maturity is set at build time from the component registry. It is never set by hand in templates.
When passing a component to an LLM for generation, extension, or debugging, the prompt should include the component’s data attributes and its token file as context. This replaces the need to describe visual properties in natural language. The machine reads the contract directly.
BEM
CEL
Naming convention
block__element--modifier
context__element--layer
Token awareness
None
Required — every class has a token
Semantic layer
Not specified
Primitive → Semantic → Component
Machine contract
None
data-cel-* attributes
AI readability
Low — inferred from CSS
High — explicit token contract
State management
Modifier on element
Layer with matching token suffix
Component maturity
Not tracked
data-cel-maturity on root
File structure
Convention-agnostic
Mirrors token and component tree
context__element--layer
nav__item--active
│ │ └ Layer: state or variant, matches a token suffix
│ └ Element: structural role inside the context
└ Context: component scope, root of the token namespace
card__heading__sub
No nested elements. Flatten to card__subheading.
card--featured--large
No stacked layers in one class. Use two classes.
card__heading { color: #1a1a1a }
No hardcoded values. Use a component token.
--btn-bg: var(--color-blue-500)
No primitive skip. Route through semantic layer.
data-cel-maturity="stable" (manual)
Maturity is set by the registry, not by hand.
• Name the Context. One word, lowercase, no abbreviations.
• List every Element. Name by role, not tag.
• List every Layer. Name by condition or variant.
• Write the primitive tokens first.
• Write the semantic tokens. Map each primitive to a role.
• Write the component tokens. One per property per element/layer.
• Write the CSS. Every rule consumes only component tokens.
• Add data-cel-* attributes to the root element.
• Register the component with a maturity stage of draft.
• Promote to stable only after token contract review.
Hamish Duncan runs HUX, a British design systems practice. He teaches operator-led no-code workshops for teams who need to scale without chaos. Build at the speed of thought.