@n8mills/design-tokens 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +429 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/components.css +943 -0
- package/design-system.md +466 -0
- package/package.json +58 -0
- package/tokens.css +498 -0
- package/tokens.d.ts +266 -0
- package/tokens.js +265 -0
- package/tokens.json +441 -0
package/design-system.md
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# Nate Mills Design System
|
|
2
|
+
|
|
3
|
+
This is the system that powers my portfolio: the philosophy, the token model, the rules, and the
|
|
4
|
+
file map. It is small on purpose, and strict on purpose. If you came to see how a one-person design
|
|
5
|
+
system stays consistent without a framework or a build pipeline you have to babysit, this is the
|
|
6
|
+
whole thing.
|
|
7
|
+
|
|
8
|
+
**The bar.** This is a portfolio system, not a utilitarian toolkit. It is built to clear the bar,
|
|
9
|
+
not scrape it: considered polish, a little playfulness, and engineering rigour, from one person
|
|
10
|
+
doing design, systems, and build.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Philosophy
|
|
15
|
+
|
|
16
|
+
A design system is easy to start and hard to keep honest. Most do not die at launch. They fall
|
|
17
|
+
apart later, quietly, when nobody is looking: a stray hex here, a one-off font size there, a card
|
|
18
|
+
that drifts a few pixels from its siblings. This system is built so that drift is hard to introduce
|
|
19
|
+
and easy to catch.
|
|
20
|
+
|
|
21
|
+
Five values shape every decision:
|
|
22
|
+
|
|
23
|
+
- **One source, one edit.** Every value lives once, in `tokens.json`. Change a primitive and every
|
|
24
|
+
semantic and component that references it follows. Reskinning the brand is a single line.
|
|
25
|
+
- **Accessible by default.** Accessibility is a constraint the system enforces, not a checklist bolted
|
|
26
|
+
on at the end. Tokens that cannot pass contrast are simply not allowed where they would fail.
|
|
27
|
+
- **Restraint over reach.** One accent, a tuned neutral scale, two fonts. No states the portfolio
|
|
28
|
+
does not use, no themes it does not need. The smaller the system, the easier it is to govern.
|
|
29
|
+
- **Plain platform.** CSS custom properties, semantic HTML, no runtime framework. The whole thing
|
|
30
|
+
loads in one network round trip and works even when opened as a local file.
|
|
31
|
+
- **Borrowed and proven, custom where it counts.** The architecture and the naming conventions follow
|
|
32
|
+
the leaders (DTCG, Material 3, Polaris, Tailwind), under one rule: match the naming to the scale,
|
|
33
|
+
words for small fixed sets, numbers for open ramps, intent for roles. The palette and the
|
|
34
|
+
proportions are hand-tuned for this portfolio. Eclectic on purpose, not by accident.
|
|
35
|
+
|
|
36
|
+
**Who this is for.** Engineers reading the token and component layers, designers reading the scales
|
|
37
|
+
and rules, and anyone evaluating whether the claims this portfolio makes about systems are real.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Getting started
|
|
42
|
+
|
|
43
|
+
- **If you build interfaces:** start with `tokens.css` (the generated variables) and `components.css`
|
|
44
|
+
(the classes that consume them). Read the semantic table below to learn which role token to reach
|
|
45
|
+
for.
|
|
46
|
+
- **If you design:** start with the Typography, Spacing, and Colour sections. The scales are short
|
|
47
|
+
and the reasoning for each is written down.
|
|
48
|
+
- **If you write product copy:** jump to Content Voice and Tone at the end. The system governs words
|
|
49
|
+
as well as pixels.
|
|
50
|
+
- **If you maintain the system:** read the Contribution Rules and the Guardrails. They are the
|
|
51
|
+
difference between a system and a stylesheet.
|
|
52
|
+
|
|
53
|
+
For every token value in full detail, see the live Under-the-Hood page. The token tables in this
|
|
54
|
+
document and the live page are both generated straight from `tokens.json` and drift-gated in CI;
|
|
55
|
+
when a text description and a generated table disagree, trust the table.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 1. The token model
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Tier 1 PRIMITIVES Raw values, named by hue and stop. No meaning on their own.
|
|
63
|
+
Tier 2 SEMANTICS Roles. Reference primitives through var(). Consumed by every component.
|
|
64
|
+
Tier 3 COMPONENTS CSS classes that read semantics only. The only layer that styles markup.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The rule is one-way: components read semantics, semantics read primitives, primitives are literals.
|
|
68
|
+
No component reads a primitive directly. The one exception is the `[data-theme="dark"]` block, which
|
|
69
|
+
re-binds the same semantic names to a different set of primitives. This is the same shape that
|
|
70
|
+
Polaris, Carbon, and Material 3 standardise around, for the same reason: when you change a primitive,
|
|
71
|
+
every semantic that references it follows, and every component that reads those semantics follows.
|
|
72
|
+
|
|
73
|
+
**The pipeline.** The tiers are authored once in `tokens.json`, in the
|
|
74
|
+
[W3C Design Tokens Community Group](https://www.designtokens.org/) (DTCG) format. `tokens.css` is
|
|
75
|
+
generated from that source by [Style Dictionary](https://styledictionary.com), the open-source
|
|
76
|
+
standard token build tool. The same source could also emit iOS, Android, or JS from one edit; here
|
|
77
|
+
it emits the CSS the site consumes. The workflow is simple: edit `tokens.json`, run the build, never
|
|
78
|
+
hand-edit `tokens.css`. A drift guard rebuilds the tokens at deploy time and refuses to ship if the
|
|
79
|
+
committed CSS does not match what the source generates.
|
|
80
|
+
|
|
81
|
+
Honest scope note: DTCG is a Community Group format, the industry's interoperability standard, not a
|
|
82
|
+
formal W3C Recommendation, and the colour module is still settling. "DTCG-format" is accurate;
|
|
83
|
+
"fully spec-compliant" would not be.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 2. Primitives
|
|
88
|
+
|
|
89
|
+
Defined once at `:root` in the generated `tokens.css`.
|
|
90
|
+
|
|
91
|
+
**Neutrals.** A custom 15-stop scale, tuned so the brand ink (`--neutral-850 #1c1c1f`) sits a touch
|
|
92
|
+
warmer than Tailwind's zinc-900. A few half-steps (`-150`, `-550`, `-850`) preserve values that the
|
|
93
|
+
rendered design actually lands on but that do not fall on a clean grid.
|
|
94
|
+
|
|
95
|
+
| Token | Hex | Where it shows up |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `--neutral-0` | `#ffffff` | Card surface, text on accent |
|
|
98
|
+
| `--neutral-100` | `#f5f5f5` | Page background (brand off-white) |
|
|
99
|
+
| `--neutral-300` | `#e4e4e7` | Hairline border |
|
|
100
|
+
| `--neutral-700` | `#3f3f46` | Secondary text (light) |
|
|
101
|
+
| `--neutral-850` | `#1c1c1f` | Brand ink, accent (light), subtle surface (dark) |
|
|
102
|
+
| `--neutral-950` | `#0a0a0b` | Page background (dark), primary text (light) |
|
|
103
|
+
|
|
104
|
+
**Green (lime).** The only colour primitive the brand needs. `--green-500 #d2ff37` is the brand
|
|
105
|
+
lime; the rest are derived for hover and dim states.
|
|
106
|
+
|
|
107
|
+
**Authored in OKLCH.** The colour primitives are written in OKLCH, the perceptual colour space from
|
|
108
|
+
CSS Color Module Level 4 (by Björn Ottosson), as DTCG colour objects that also carry the original
|
|
109
|
+
hex. The build emits a hex fallback line then the `oklch()`, so the colour is identical everywhere
|
|
110
|
+
and pre-2023 browsers still get the hex. The OKLCH values are exact conversions of the hand-tuned
|
|
111
|
+
hexes, verified to land on the same pixel, not a regenerated ramp, so the lime keeps its character.
|
|
112
|
+
|
|
113
|
+
**What is deliberately missing.** No emerald, amber, or red. A portfolio has no warning or error UI,
|
|
114
|
+
and padding the system with state colours no component reads would be the opposite of the discipline
|
|
115
|
+
it is meant to show. If a future consumer needs them, the rule is: add at the primitive layer, route
|
|
116
|
+
a semantic, then consume that. Lime plus neutrals is the entire palette.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 3. Semantics
|
|
121
|
+
|
|
122
|
+
Roles that reference primitives through `var()`. Every component reads these, never raw hex.
|
|
123
|
+
|
|
124
|
+
_Nine tokens shown here. The full set of surface, text, border, and accent tokens is on the live Under-the-Hood page._
|
|
125
|
+
|
|
126
|
+
<!-- TOKENS:pub-semantics:start -->
|
|
127
|
+
|
|
128
|
+
| Token | Light | Dark |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `--color-bg` | `--neutral-100` | `--neutral-950` |
|
|
131
|
+
| `--color-surface` | `--neutral-0` | `--neutral-850` |
|
|
132
|
+
| `--color-surface-sunken` | `--neutral-200` | `--neutral-900` |
|
|
133
|
+
| `--color-text-primary` | `--neutral-950` | `--neutral-150` |
|
|
134
|
+
| `--color-text-secondary` | `--neutral-700` | `--neutral-500` |
|
|
135
|
+
| `--color-border` | `--neutral-300` | `--neutral-800` |
|
|
136
|
+
| `--color-border-brand` | `--brand-lime` | same |
|
|
137
|
+
| `--color-accent` | `--neutral-850` | `--green-500` |
|
|
138
|
+
| `--color-focus-ring` | `--neutral-850` | `--green-500` |
|
|
139
|
+
|
|
140
|
+
<!-- TOKENS:pub-semantics:end -->
|
|
141
|
+
|
|
142
|
+
**Brand identity aliases** sit alongside the role tokens: `--brand-lime` (`var(--green-500)`),
|
|
143
|
+
`--brand-ink` (`var(--neutral-850)`), and the paired "on" tokens that name the correct text colour
|
|
144
|
+
for a fixed-colour surface (`--brand-on-lime` resolves to ink, `--brand-on-ink` resolves to lime).
|
|
145
|
+
|
|
146
|
+
**The one rule worth memorising.** The base accent is ink, not colour. Lime is a background and
|
|
147
|
+
accent, never foreground text on a light surface. The contrast math forces it: `#d2ff37` on
|
|
148
|
+
`#f5f5f5` is far below the 4.5 to 1 floor (see the live Under-the-Hood page for current ratio). So the system gives you the right tool instead:
|
|
149
|
+
`--color-accent-emphasis` (theme-aware, resolves to ink on light and lime on dark) for brand-tinted
|
|
150
|
+
words in running copy, and the "on" tokens for text on fixed lime or ink surfaces. If you reach for
|
|
151
|
+
`color: var(--brand-lime)` outside a known-dark scope, it is almost always the wrong token.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 4. Typography
|
|
156
|
+
|
|
157
|
+
**Two fonts, no more.** Inter for everything; JetBrains Mono for labels, eyebrows, code, and tabular
|
|
158
|
+
numbers. PT Serif appears in exactly one place: the testimonial pull quotes.
|
|
159
|
+
|
|
160
|
+
The lean semantic scale that new work consumes:
|
|
161
|
+
|
|
162
|
+
<!-- TOKENS:pub-type-semantic:start -->
|
|
163
|
+
|
|
164
|
+
| Token | Value | Used for |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `--text-h1` | `clamp(34px, 5.8vw, 72px)` | Page-level section headings (every section h2). The hero h1 is a documented exception with its own mobile-tuned clamp. |
|
|
167
|
+
| `--text-h2` | `clamp(22px, 4vw, 32px)` | Card titles, intro lines, sub-headings inside section bodies. |
|
|
168
|
+
| `--text-body` | `--size-base` | Paragraphs, lists, default reading text, card body copy. |
|
|
169
|
+
| `--text-label` | `--size-2xs` | Uppercase mono labels, eyebrows, stat labels, badges. |
|
|
170
|
+
| `--text-caption` | `--size-xs` | Tiny captions, footnotes, stat descriptions. |
|
|
171
|
+
| `--text-nav` | `--size-2xs` | Header menu links; equals --text-label today, kept separate so nav can diverge. |
|
|
172
|
+
|
|
173
|
+
<!-- TOKENS:pub-type-semantic:end -->
|
|
174
|
+
|
|
175
|
+
Headings use fluid `clamp()` so type scales smoothly between a mobile floor and a desktop ceiling
|
|
176
|
+
without per-breakpoint overrides. If you find yourself reaching for a media query to nudge a font
|
|
177
|
+
size, the floor or the ceiling is wrong; fix the clamp, not the breakpoint.
|
|
178
|
+
|
|
179
|
+
Five weights are loaded from Inter (400 to 800). One emphasis rule matters: a full italic display
|
|
180
|
+
heading is banned. Italic on a single word inside a heading is allowed, paired with a solid muted
|
|
181
|
+
token, never an alpha-modified colour (overlapping italic glyphs at tight tracking double the alpha
|
|
182
|
+
and leave visible dark spots at the letter joins).
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 5. Spacing
|
|
187
|
+
|
|
188
|
+
A doubling rhythm from 4 to 256 px, with two medium-tight additions for breaks that need them.
|
|
189
|
+
|
|
190
|
+
<!-- TOKENS:pub-spacing:start -->
|
|
191
|
+
|
|
192
|
+
| Token | Value | Description |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `--space-1` | `4px` | Hairline gap. Icon-to-label, tight inline spacing. |
|
|
195
|
+
| `--space-2` | `8px` | Tight gap. Chip padding, small stacks. |
|
|
196
|
+
| `--space-3` | `12px` | Medium-tight break (added between 2 and 4). Heading-to-body, compact rows. |
|
|
197
|
+
| `--space-4` | `16px` | Base unit. Default gap between related elements; the compact card-padding tier (--card-pad-compact). |
|
|
198
|
+
| `--space-5` | `24px` | Medium break (added between 4 and 6). Group separation inside a section; the standard card-padding tier (--card-pad-standard). |
|
|
199
|
+
| `--space-6` | `32px` | Large gap. Between subsections and card to card; the spacious card-padding tier (--card-pad-spacious). |
|
|
200
|
+
| `--space-7` | `48px` | Block spacing between stacked content blocks. |
|
|
201
|
+
| `--space-8` | `64px` | Major block separation and section inner padding. |
|
|
202
|
+
| `--space-9` | `96px` | Section rhythm. The lower bound of vertical section padding (--section-padding). |
|
|
203
|
+
| `--space-10` | `128px` | Section rhythm. The upper bound of vertical section padding (--section-padding). |
|
|
204
|
+
| `--space-11` | `192px` | Outsized spacing for full-bleed editorial breaks. |
|
|
205
|
+
| `--space-12` | `256px` | Largest step. Rare, marquee-scale vertical space. |
|
|
206
|
+
|
|
207
|
+
<!-- TOKENS:pub-spacing:end -->
|
|
208
|
+
|
|
209
|
+
Never use raw px for spacing in a component. The whole point is one place to edit the rhythm.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 6. Radii, borders, shadows
|
|
214
|
+
|
|
215
|
+
<!-- TOKENS:pub-radii-borders-shadows:start -->
|
|
216
|
+
|
|
217
|
+
**Radii**
|
|
218
|
+
|
|
219
|
+
| Token | Value | Description |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| `--radius-none` | `0` | Square. No rounding, for sharp-cornered surfaces and full-bleed media. |
|
|
222
|
+
| `--radius-sm` | `8px` | Small radius. The workhorse for inputs, chips, and small controls. |
|
|
223
|
+
| `--radius-md` | `10px` | Medium radius. Mid-size controls and insets, a touch rounder than sm. |
|
|
224
|
+
| `--radius-lg` | `14px` | Large radius. The default for cards and modals. |
|
|
225
|
+
| `--radius-xl` | `20px` | Extra-large radius. Prominent panels and feature surfaces. |
|
|
226
|
+
| `--radius-full` | `999px` | Full round. Pills, tags, and circular buttons; forces a complete radius at any height. |
|
|
227
|
+
|
|
228
|
+
**Borders**
|
|
229
|
+
|
|
230
|
+
| Token | Value | Description |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `--border-hairline` | `1px solid var(--color-border)` | Default 1px hairline border; reads --color-border so it follows the theme. The standard card edge and divider. |
|
|
233
|
+
| `--border-strong` | `1px solid var(--color-border-strong)` | Heavier 1px border; reads --color-border-strong. For edges that need to read past a hairline, such as interactive-card hover outlines. (Button borders moved to the btn.* component tokens in Task 87.) |
|
|
234
|
+
| `--border-focus` | `2px solid var(--color-focus-ring)` | 2px focus outline; reads --color-focus-ring (ink on light, lime on dark). The keyboard-focus indicator, never removed without a replacement. |
|
|
235
|
+
| `--focus-ring-offset` | `2px` | Offset between an element and its focus ring. Outsets the 2px ring so it clears the element edge. |
|
|
236
|
+
|
|
237
|
+
**Shadows**
|
|
238
|
+
|
|
239
|
+
| Token | Value | Description |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `--shadow-none` | `none` | No shadow. The default; the system favours borders over shadows. |
|
|
242
|
+
| `--shadow-soft` | `0 1px 2px rgb(var(--brand-ink-rgb) / 0.04), 0 1px 3px rgb(var(--brand-ink-rgb) / 0.06)` | Subtle resting elevation, two stacked ink-alpha layers, for cards that lift just off the page. Dark mode swaps to deeper black-alpha. |
|
|
243
|
+
| `--shadow-lift` | `0 4px 12px rgb(var(--brand-ink-rgb) / 0.06), 0 2px 4px rgb(var(--brand-ink-rgb) / 0.04)` | Stronger elevation for overlays and float-on-scroll chrome only, not resting cards. |
|
|
244
|
+
| `--shadow-modal` | `0 24px 64px rgba(0, 0, 0, 0.5), 0 8px 16px rgba(0, 0, 0, 0.18)` | Modal and lightbox elevation. The deepest shadow, for surfaces floating above a scrim. |
|
|
245
|
+
| `--accent-period-shadow` | `0 1px 2px rgba(0,0,0,0.42), 0 0 0 1px rgba(0,0,0,0.18)` | Depth under the lime period in section titles. Keeps the lime dot legible on light surfaces; resolves to none in dark, where lime already reads. |
|
|
246
|
+
| `--accent-dot-shadow` | `0 1px 3px rgba(0,0,0,0.24)` | Depth under the active carousel pagination dot. Keeps the lime dot legible on light surfaces; resolves to none in dark. |
|
|
247
|
+
|
|
248
|
+
<!-- TOKENS:pub-radii-borders-shadows:end -->
|
|
249
|
+
|
|
250
|
+
The aesthetic is Swiss-minimal, so borders carry structure and shadows are rare. Two small
|
|
251
|
+
accent-shadow tokens keep the lime accents legible on light backgrounds and resolve to `none` in
|
|
252
|
+
dark mode, where lime already reads cleanly.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 7. Motion
|
|
257
|
+
|
|
258
|
+
<!-- TOKENS:pub-motion:start -->
|
|
259
|
+
|
|
260
|
+
**Durations**
|
|
261
|
+
|
|
262
|
+
| Token | Value | Used for |
|
|
263
|
+
|---|---|---|
|
|
264
|
+
| `--duration-fast` | `150ms` | Hover, focus, and micro state feedback. |
|
|
265
|
+
| `--duration-base` | `220ms` | Reveal, modal, and swap transitions. |
|
|
266
|
+
| `--duration-slow` | `400ms` | Context shift and slide transitions. |
|
|
267
|
+
| `--duration-expressive` | `1300ms` | Expressive tier: the slow, cinematic duration for a single large hero moment (the two-phone Spline scene scales in over this). Deliberately longer than the general-purpose slow tier and reserved for one large-surface entrance, never UI feedback. Collapsed to 1ms under reduced motion by the block in tokens/build.mjs. |
|
|
268
|
+
|
|
269
|
+
**Easings**
|
|
270
|
+
|
|
271
|
+
| Token | Value | Used for |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `--ease-default` | `cubic-bezier(0.4, 0, 0.2, 1)` | The workhorse symmetric curve. Begins and ends on screen. |
|
|
274
|
+
| `--ease-entrance` | `cubic-bezier(0, 0, 0.2, 1)` | Decelerate, for elements entering the screen. |
|
|
275
|
+
| `--ease-exit` | `cubic-bezier(0.4, 0, 1, 1)` | Accelerate, for elements leaving the screen. |
|
|
276
|
+
| `--ease-emphasized` | `cubic-bezier(0.16, 1, 0.3, 1)` | Pronounced ease-out for overlay and menu entrance motion (items rising into view). A stronger, slower-settling decelerate than --ease-entrance. |
|
|
277
|
+
| `--ease-linear` | `cubic-bezier(0, 0, 1, 1)` | Constant rate, no acceleration. For continuous loop motion: the hero ticker, both marquees, the glass column scroll, and the loading-icon cycles. Authored as a cubic-bezier twin of the linear keyword so it lives in the ease group with its siblings. |
|
|
278
|
+
|
|
279
|
+
<!-- TOKENS:pub-motion:end -->
|
|
280
|
+
|
|
281
|
+
A lean ladder: three general-purpose steps plus one `expressive` tier for a single large hero moment, and role-named easings.
|
|
282
|
+
|
|
283
|
+
Every animation respects `prefers-reduced-motion`. A single override at the bottom of `tokens.css`
|
|
284
|
+
collapses every duration to 1 ms and disables transforms, so honouring the preference is automatic,
|
|
285
|
+
not per-component.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 8. Layout
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
--container-base 960px (prose, single-column sections)
|
|
293
|
+
--container-wide clamp(1200px, 92vw, 1440px) (galleries, grids)
|
|
294
|
+
|
|
295
|
+
--bp-mobile 375px --bp-tablet 768px --bp-desktop 1024px --bp-wide 1440px
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Sections run full-bleed at the outer level and centre their content in one of the two container
|
|
299
|
+
widths, so every section's gutter aligns.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 9. Exemplar components
|
|
304
|
+
|
|
305
|
+
Components live in `components.css`, read semantic tokens only, and document their variants. Three
|
|
306
|
+
are worth showing in full, because they carry most of the system's decisions.
|
|
307
|
+
|
|
308
|
+
### Button
|
|
309
|
+
|
|
310
|
+
| Variant | Background | Text | Use on |
|
|
311
|
+
|---|---|---|---|
|
|
312
|
+
| `.btn--primary` | `--brand-lime` | `--brand-ink` | Off-white surfaces |
|
|
313
|
+
| `.btn--secondary` | transparent | `--color-text-primary` | Lime surfaces |
|
|
314
|
+
| `.btn--icon` | transparent | `--color-text-secondary` | Chrome (toggles, controls) |
|
|
315
|
+
|
|
316
|
+
The pairing rule is strict: lime-on-lime is forbidden, so `.btn--primary` never sits on a lime
|
|
317
|
+
background; the secondary variant carries contrast there with ink text and a border. Icons trail the
|
|
318
|
+
label and map to action type: `mail` for `mailto:`, `external-link` for outbound links, `download`
|
|
319
|
+
for files, no icon for same-page anchors. `arrow-up-right` is not a substitute for `external-link`;
|
|
320
|
+
they mean different things to a screen-reader user.
|
|
321
|
+
|
|
322
|
+
### Card
|
|
323
|
+
|
|
324
|
+
`.card` is the light-surface base: off-white background, hairline border, soft radius. `.card--dark`
|
|
325
|
+
is the modifier for dark slabs: it swaps the surface to ink, the text to the on-ink tokens, and the
|
|
326
|
+
border to the on-ink hairline, while inheriting the radius and border style from `.card`. Compose it
|
|
327
|
+
as `class="card card--dark"`. A surface that must stay dark in both themes uses these fixed tokens,
|
|
328
|
+
never a pinned `data-theme`, because a pinned theme propagates when an element is cloned and silently
|
|
329
|
+
breaks dark mode.
|
|
330
|
+
|
|
331
|
+
### The section heading triplet
|
|
332
|
+
|
|
333
|
+
Every section uses one heading pattern: a label, a title, and a subtitle. Shape signals role across
|
|
334
|
+
the whole system: pills are identity (status, taxonomic tags), rounded rectangles are actions
|
|
335
|
+
(buttons, cards), circles are people and brands (avatars, social icons), squares are chrome
|
|
336
|
+
utilities (toggles). Mixing those metaphors is the fastest way to make an interface feel arbitrary,
|
|
337
|
+
so the system names the rule and holds to it.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## 10. Accessibility
|
|
342
|
+
|
|
343
|
+
WCAG 2.2 AA is the floor here, not the ceiling. WCAG 2.2 is a W3C Recommendation, and since October
|
|
344
|
+
2025 it is also the international standard ISO/IEC 40500:2025. Every AA claim on this site is a
|
|
345
|
+
self-assessment, backed by an axe-core gate that runs on both themes on every deploy, not a
|
|
346
|
+
third-party certification.
|
|
347
|
+
|
|
348
|
+
I also explore APCA, the perceptual contrast model, as a forward-looking readability ceiling above
|
|
349
|
+
that floor. APCA is not a conformance standard: it was pulled from the WCAG 3 Working Draft in 2023,
|
|
350
|
+
and WCAG 3 is still years from being a standard, so I treat APCA as a sharper readability check,
|
|
351
|
+
never a compliance claim. The short version: WCAG 2.2 AA as the floor, APCA explored as a
|
|
352
|
+
forward-looking readability ceiling.
|
|
353
|
+
|
|
354
|
+
- WCAG 2.2 AA contrast for every text token. The token source is contrast-linted in CI (a blocking
|
|
355
|
+
Terrazzo check over the semantic pairs in both themes), and every swatch on the live
|
|
356
|
+
Under-the-Hood page computes its ratio from the rendered tokens at load.
|
|
357
|
+
- The focus ring is a 2 px brand outline, never removed without a replacement.
|
|
358
|
+
- Touch targets clear 44 by 44 px.
|
|
359
|
+
- Every animation is gated behind `prefers-reduced-motion`.
|
|
360
|
+
- Semantic HTML first; ARIA only where HTML cannot carry the meaning.
|
|
361
|
+
- Lime is forbidden as foreground text on light surfaces. Decorative bars, the brand dot, and
|
|
362
|
+
dark-slab accents only.
|
|
363
|
+
- **Never dim accessible text with `opacity`.** Opacity multiplies against whatever sits behind the
|
|
364
|
+
element and silently drops a passing colour below 4.5 to 1. The token looks right in the source and
|
|
365
|
+
fails on screen. Choose a lighter text token that itself clears the floor.
|
|
366
|
+
- Every `<img>` declares `width` and `height` to prevent layout shift, which disorients screen-reader
|
|
367
|
+
and magnifier users tracking position.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 11. File map
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
tokens.json The single source of truth (DTCG format)
|
|
375
|
+
tokens.css Generated CSS custom properties (primitives + semantics)
|
|
376
|
+
components.css The component layer
|
|
377
|
+
design-system.md This document
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`tokens.json` is the source; `tokens.css` is generated and never hand-edited. (An inline token
|
|
381
|
+
mirror existed early on; it was removed when the tokens moved to the generated external file.)
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 12. Contribution rules
|
|
386
|
+
|
|
387
|
+
1. Never write raw hex outside `tokens.css`. Define a primitive or use a semantic.
|
|
388
|
+
2. Brand tokens are aliases, not duplicates: `--brand-lime: var(--green-500)`, not the hex.
|
|
389
|
+
3. Components read semantics only, never a raw primitive.
|
|
390
|
+
4. Test in light and dark. Every semantic has both.
|
|
391
|
+
5. WCAG 2.2 AA for every text colour, against every surface it could land on.
|
|
392
|
+
6. Every animation gets a reduced-motion collapse.
|
|
393
|
+
7. One container width per section.
|
|
394
|
+
8. The label, title, subtitle triplet is the only heading pattern.
|
|
395
|
+
9. No full italic display headings.
|
|
396
|
+
10. Be explicit about which viewport a CSS change targets, and verify at three widths before calling
|
|
397
|
+
it done.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## 13. What this system is NOT
|
|
402
|
+
|
|
403
|
+
- Not a component library. The custom build is the point; there is no DaisyUI or Material under it.
|
|
404
|
+
- Not Tailwind-driven. Tailwind is a one-off layout convenience, not the system.
|
|
405
|
+
- Not runtime CSS-in-JS. Plain custom properties, one network round trip.
|
|
406
|
+
- Not theme-able beyond light and dark. The Bupa case study scopes blue back inside its own section;
|
|
407
|
+
that is the only third theme, and it is section-scoped, never global.
|
|
408
|
+
|
|
409
|
+
Fork it for another brand by changing `--green-500` and watching every consumer follow. That is the
|
|
410
|
+
system working as designed.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 14. Guardrails
|
|
415
|
+
|
|
416
|
+
The mistakes that have actually surfaced, kept here so they do not surface again.
|
|
417
|
+
|
|
418
|
+
- No blue outside the Bupa case study. The base accent is ink.
|
|
419
|
+
- Inter only. PT Serif for pull quotes, JetBrains Mono for labels and numbers.
|
|
420
|
+
- No full italic display headings.
|
|
421
|
+
- Surfaces are off-white or near-black only. No cream, no tinted "warm" backgrounds.
|
|
422
|
+
- Lime is never foreground text on a light surface.
|
|
423
|
+
- No raw values in components. Every colour, space, radius, and duration resolves to a token.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 15. Content voice and tone
|
|
428
|
+
|
|
429
|
+
Consistency is linguistic, not just visual. A system that governs pixels but lets copy wander still
|
|
430
|
+
feels inconsistent. So the words follow one rule, the same way the colours do.
|
|
431
|
+
|
|
432
|
+
### One voice
|
|
433
|
+
|
|
434
|
+
I write the way I'd talk to another designer over coffee: plain words, short sentences, say the
|
|
435
|
+
thing. No jargon, no sales pitch, no fake excitement. First person. Proof carries the brag: "18
|
|
436
|
+
hours back to the team," not "huge productivity gains." Warmth comes from honesty, not enthusiasm.
|
|
437
|
+
No hedges.
|
|
438
|
+
|
|
439
|
+
### Only the length changes
|
|
440
|
+
|
|
441
|
+
The voice never changes; only the length does. Body copy gets whole sentences with room to breathe
|
|
442
|
+
(6 to 14 words each). Buttons and microcopy compress to verb plus outcome: "Read the case study,"
|
|
443
|
+
"Get in touch," never "Submit" or "Learn more." Errors name the problem and the workaround with no
|
|
444
|
+
apology preamble. Status pills are declarative present tense ("Available for work"), never
|
|
445
|
+
aspirational. Internal docs get the same voice at note length: present-tense facts, decisions
|
|
446
|
+
explained, not defended.
|
|
447
|
+
|
|
448
|
+
### Word list
|
|
449
|
+
|
|
450
|
+
| Use | Avoid |
|
|
451
|
+
|---|---|
|
|
452
|
+
| system, token, governance, pipeline, foundation | passionate, world-class, cutting-edge, seamless |
|
|
453
|
+
| ship, scale, adoption, contribution | amazing, incredible, excited |
|
|
454
|
+
| multi-brand, cross-platform, token-driven | we believe, we think, arguably |
|
|
455
|
+
| let's talk, get in touch | click here, learn more, submit |
|
|
456
|
+
|
|
457
|
+
### Copy patterns
|
|
458
|
+
|
|
459
|
+
- **CTA buttons:** verb plus outcome. "Read the case study," not "Click to learn more."
|
|
460
|
+
- **Section subtitle:** one sentence, sets the stake, 18 words or fewer.
|
|
461
|
+
- **Error message:** what happened plus the workaround, no "sorry."
|
|
462
|
+
- **Empty state:** what is missing, what to do.
|
|
463
|
+
- **Loading state:** set an expectation if it runs over a second, otherwise no copy.
|
|
464
|
+
|
|
465
|
+
Across all three registers: numbers do the bragging, headings are sentence case, and superlatives
|
|
466
|
+
stay out.
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@n8mills/design-tokens",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The design tokens behind natemills.me: W3C DTCG source, generated CSS custom properties, the component layer, and a resolved JS/TS export. Two-tier, OKLCH-authored, WCAG 2.2 AA.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Nate Mills <mail@natemills.me> (https://natemills.me)",
|
|
7
|
+
"homepage": "https://github.com/n8mills-UI/nate-mills-design-system#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/n8mills-UI/nate-mills-design-system.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/n8mills-UI/nate-mills-design-system/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"design-tokens",
|
|
17
|
+
"design-system",
|
|
18
|
+
"dtcg",
|
|
19
|
+
"w3c-design-tokens",
|
|
20
|
+
"css-variables",
|
|
21
|
+
"custom-properties",
|
|
22
|
+
"oklch",
|
|
23
|
+
"style-dictionary",
|
|
24
|
+
"tokens",
|
|
25
|
+
"theme"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": [
|
|
29
|
+
"*.css"
|
|
30
|
+
],
|
|
31
|
+
"main": "./tokens.js",
|
|
32
|
+
"module": "./tokens.js",
|
|
33
|
+
"types": "./tokens.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./tokens.d.ts",
|
|
37
|
+
"import": "./tokens.js",
|
|
38
|
+
"default": "./tokens.js"
|
|
39
|
+
},
|
|
40
|
+
"./css": "./tokens.css",
|
|
41
|
+
"./tokens.css": "./tokens.css",
|
|
42
|
+
"./components.css": "./components.css",
|
|
43
|
+
"./tokens.json": "./tokens.json",
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"tokens.js",
|
|
48
|
+
"tokens.d.ts",
|
|
49
|
+
"tokens.css",
|
|
50
|
+
"tokens.json",
|
|
51
|
+
"components.css",
|
|
52
|
+
"design-system.md",
|
|
53
|
+
"DESIGN.md"
|
|
54
|
+
],
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|