# Design Library Architecture
**Sample code** — the actual source files this note examines, in the public
ngx-experiments repo:
- **Component** — dlc-button.component.ts
- **SCSS** — dlc-button.component.scss
- **Theme** — _dlc-professional-light.scss
A design library is best understood as two libraries rather than one. The
components live in a **design library**; the values that color them live in a
separate **themes** library. Between them lies nothing but a contract of CSS
custom properties — no import, no shared build. That seam is the entire design.
It is what allows one set of components to wear six different skins, to restyle
with a class swap rather than a recompile, to encode a domain's own vocabulary in
its tokens, and still tree-shake to only what an application imports.
Everything below is grounded in two real, public libraries built to the same
spine: a shared design system (`dlc-` prefix — the `@nge/ui-design-library`
components over the `@nge/themes` token sets) and a personal-finance domain built
on top of it (`ldg-` prefix). One *defines* its values from scratch; the other
*inherits* them and adds only its own — and that contrast is the point.
## Two libraries, one contract
Draw the line in the right place and the coupling disappears. The design library
knows *structure and behavior* — a button is a button, a card is a card. The
themes library knows *values* — one persona's primary is navy, another's is sage.
Neither library imports the other. They meet at a namespace of CSS variables —
`--dlc-*` for the shared library, `--ldg-*` for a domain such as ledger.
A component **reads** the contract:
```scss
/* design library — dlc-button reads a token without knowing its eventual value */
.dlc-button--primary button {
background-color: var(--dlc-secondary);
color: var(--dlc-on-secondary);
}
```
A theme **supplies** it:
```scss
/* themes — .dlc-professional-light supplies values for the same names */
.dlc-professional-light {
--dlc-secondary: #c4841d; /* gold */
--dlc-on-secondary: #ffffff;
}
```
The contract itself is written down — a token file (`_dlc-tokens.scss`) that
declares every `--dlc-*` name and what it means, with the actual values living in
the persona files. The component depends on the *names*; the theme owns the
*values*; the token file is the treaty both sign. Adding a component touches no
theme; adding a theme touches no component. The five sections that follow are all
consequences of that one cut.
## Encapsulation, inverted
Angular's default is to scope a component's styles to itself — emulated
encapsulation rewrites the component's selectors so they cannot leak out *or in*.
A design library wants the opposite, so every library component opts out:
```ts
@Component({
encapsulation: ViewEncapsulation.None,
host: { class: 'dlc-button' },
selector: 'dlc-button',
// …
})
```
That is a real, shipped component — `dlc-button` from `@nge/ui-design-library`,
with its source linked at the top of this note.
With `ViewEncapsulation.None` there is no automatic scoping, so the library
supplies its own by hand: a `host` class matching the selector, and SCSS wrapped
in that same class — never `:host`, which does not behave under `None`.
```scss
.dlc-button { /* wrapper class, matches host — the manual scope */
button {
border-radius: var(--dlc-radius-md);
font-family: var(--dlc-font-family-body);
}
}
```
Why give up the framework's scoping? Because emulated encapsulation would wall
off the very thing the contract depends on. CSS custom properties cascade through
the DOM tree, and a theme is simply those properties declared high on the
document root. With encapsulation turned off, the token values flow freely from
`` down into every component; the wrapper class keeps the library's *own*
rules from leaking while letting the *tokens* through. Inverting the default is
what makes a page-level theme reach a deeply nested button at all.
## A component that needs no theme
Consider the button token again: what renders if no theme is applied? A bare
Storybook cell, a unit test, a component dropped on an empty page — none of them
carry a `.dlc-professional-light` ancestor. The answer is that every color token
carries a **literal fallback**, so a component is self-sufficient:
```scss
.ldg-account-card__button {
background: var(--ldg-card, #edf1f5);
border: 1px solid var(--ldg-border, #c4d0da);
color: var(--ldg-on-surface, #121518);
}
```
If `--ldg-card` is defined, the theme wins; if nothing defined it, the component
still paints `#edf1f5` and looks correct. The theme becomes an *override*, not a
dependency. This is also where the library draws its firmest boundary against a
framework theme runtime: these are own-namespace `---*` tokens with real
hex fallbacks, never `--mat-sys-*` and never a `mat-*` element — an approach
proven out without Angular Material anywhere in the component's styles. A
component ships knowing how to look like itself, and a theme only ever refines
that.
## A theme is a class, not a build
Because a theme is only a set of values, switching themes is a class swap, not a
rebuild. The themes library ships one class per look, each re-declaring the same
contract with different values:
```scss
.dlc-professional-light { --dlc-primary: #0f2b3c; --dlc-secondary: #c4841d; /* navy + gold */ }
.dlc-home-light { --dlc-primary: #3d7b5f; --dlc-secondary: #c4704d; /* sage + terracotta */ }
```
The shared themes library ships **six** of these — three brand personas
(professional for brokers, home for clients, service-provider for vendors) across
light and dark. An entry mixin emits them all, and the application includes it
once:
```scss
@use 'dlc-themes';
@include dlc-themes.dlc-theme-mixin();
/* then, on the document: */
```
Swapping the class on `` re-colors every component on the page in a single
paint — no recompile, no rebuilt bundle, no component aware it happened. Class
specificity beats a `:root` default, so themed values win wherever the class is
present. One detail matters here: the persona class belongs on the document root,
because the base layer styles `body` background, text, and scrollbars through the
same `var(--dlc-*)` tokens — apply the class to a nested element and those
page-level globals are never reached.
## Tokens speak the domain's language
A token layer is not limited to `primary` and `surface`. It is the place a domain
encodes its own vocabulary — the same ubiquitous language from
[Domain-Driven Design](https://jerryorta.com/concepts/domain-driven-design), reaching all the way into
the palette. The ledger domain deals in money, so it defines what money *means*
as color:
```scss
/* financial polarity — deliberately its own signal, not a validation state */
@mixin ldg-money-tokens-light() {
--ldg-money-positive: #047857; /* emerald — credits, income, balance up */
--ldg-money-negative: #be123c; /* rose — debits, expenses, balance down */
}
```
These are new values, not aliases of a generic `--success`/`--error`, and that is
intentional: a credit is not a passing form field, so financial polarity reads as
its own signal. The domain also owns a qualitative palette for its
spending-category chart — eight hues spread around the wheel, kept distinct from
the money colors so a category never reads as a gain or a loss:
```scss
@mixin ldg-category-tokens-light() {
--ldg-category-1: #2563eb; /* blue */
--ldg-category-2: #ea580c; /* orange */
/* … */
--ldg-category-8: #475569; /* slate — the "other" catch-all */
}
```
There is even a rule recorded in the token's documentation: assign categories to
slots in a fixed order and never re-point a slot's hue once data has shipped with
it, or every historical chart silently relabels itself. The palette is not
decoration — it is a stable part of the domain's contract with its own past data.
Tokens are where design meets domain modeling.
## Define it, or inherit it
Here is where the two libraries diverge, and it is the most useful contrast in the
whole design. Both ride the same spine — components read a `--prefix-*` contract,
themes supply it — but they make opposite choices about *where the values come
from*.
The shared foundation **defines**. `@nge/themes` owns the full `--dlc-*` palette
outright and writes all six persona themes from scratch — every navy, every gold,
hand-tuned. It is the base every consumer builds on.
A domain **inherits**. Ledger does not redefine a palette; its tokens are pure
passthrough aliases of that shared persona contract (`--dlc-*`):
```scss
/* ledger — one indirection per token, declared once for all six persona classes */
@mixin ldg-aliases() {
--ldg-surface: var(--dlc-surface, #f8f9fa);
--ldg-card: var(--dlc-surface-container, #edf1f5);
--ldg-primary: var(--dlc-primary, #0f2b3c);
/* … */
}
```
Custom-property indirection does the work: `--ldg-surface` resolves against
whichever `.dlc-*` persona class is active on an ancestor, so the aliases *follow*
the shared personas with no persona-switching logic of their own — declared once,
never duplicated per persona. Ledger writes fresh values only for what is
genuinely its own (the money and category tokens above). Its components follow the
same approach, composing shared `dlc-*` primitives and adding domain pieces — an
account card, a budget card — on top.
The same architecture offers two dials. The shared library defines the contract so
every domain has a floor to stand on; a domain that needs to move quickly aliases
it and overrides only its deltas. The relationship runs both ways: a component
that proves broadly useful is *promoted* up the hierarchy — from a domain library
into the shared one — because the only thing that changes is its prefix, and the
contract shape stays identical.
## The seam is also a bridge
One last dividend of making the contract nothing but CSS variables: the themes
library becomes the single place to *translate* a domain's palette into another
library's contract. A shared charts library exposes its own `--giga-chart-*` tokens
(the theming seam described in
[Charts Library Architecture](https://jerryorta.com/concepts/charts-library-architecture)). The themes
library does not fork the charts library to recolor it — each persona theme simply
binds the chart contract to its own tokens:
```scss
.dlc-professional-light {
/* --- chart token bridge --- */
--giga-chart-surface: var(--dlc-surface);
--giga-chart-primary: var(--dlc-primary);
--giga-chart-secondary: var(--dlc-secondary);
--giga-chart-tertiary: var(--dlc-warning); /* no tertiary in the palette — nearest match */
}
```
Now a shared chart renders in full persona dress — different for each of the six
personas — and the charts library never learns the theme layer exists. The same
technique recolors the Storybook review chrome (`--sb-review-*`) per persona.
Every one of these is one library's contract expressed in another's values, and
the themes layer is where that translation lives. The seam that decouples
components from values also lets whole libraries meet without depending on each
other.
## The shape an agent can extend
All of this has the same second payoff as the charts library: the pieces are
small, additive, and governed by a written contract, so change is bounded. A new
component follows a fixed recipe — a prefixed selector, a matching host class, a
wrapper-class stylesheet, tokens with literal fallbacks, a Storybook story. A new
theme is one class file re-declaring the contract. A new token is a line in the
token file plus a value in each theme. Nothing edits a central registry; there is
no shared switch to break.
That is exactly the shape I hand to Claude Code. The [skills I rely
on](https://jerryorta.com/concepts/claude-skills) encode this contract directly — one verifies (and
auto-fixes) that a component's tokens and accessibility hooks match the standard,
another promotes a component up the placement hierarchy as a pure prefix swap. The
agent works from the same best-practices document I would, and the library's own
lint and tests are the gate. The architecture that keeps the design system
coherent is the same one that makes extending it a low-risk, well-specified task.
## Why it holds together
Six decisions, one shape — and a dividend:
- **Two libraries, one contract** — components own structure, themes own values,
and a CSS-variable namespace is the only thing between them, so neither change
touches the other.
- **Encapsulation inverted** — `ViewEncapsulation.None` plus a hand-written
wrapper class lets theme tokens cascade from the document root into every
component instead of being walled off.
- **Literal fallbacks** — every token carries a real default, so a component
renders correctly with no theme at all; the theme is an override, never a
dependency (and never Angular Material).
- **A theme is a class** — one class per look re-declares the contract, so
switching among six personas is a class swap on ``, not a recompile.
- **Tokens speak the domain** — money polarity, a stable category palette; the
token layer is where a domain's language becomes color.
- **Define it or inherit it** — the shared foundation writes all six personas
from scratch; a domain aliases that contract and overrides only its deltas, on
the identical spine.
- **The seam is a bridge** — because the contract is only variables, the themes
layer can translate a domain's palette into a shared chart's or Storybook's
contract, letting libraries meet without importing each other.
The payoff is that the sixth persona costs what the first one did — and a shared
chart, a new component, and an AI agent all read from the same contract.