Today we are tagging v1.0.0 of the YPAI Design System and opening its reference site to the public at /design/. Eight weeks ago, the front end of yourpersonalai.net was a perfectly functional Astro app that had grown the way most marketing sites grow: page by page, designer by designer, opinion by opinion. The system worked. The system was also a fan-out of bespoke components held together by tribal knowledge. This post explains what we changed, why, and what we are not yet shipping.
Why we built it
The trigger was an audit, not a vision. In late February we ran a sweep of every CSS custom property declared anywhere under src/ and got a number that surprised us.
The fix could not be “rewrite everything,” because the site was healthy and shipping revenue. It also could not be “add a new token file,” because we already had six. What we needed was a single layer of canonical tokens with a documented contract, a codemod that migrated the existing 499 files to that layer, and a reference site that made the contract findable a year later when whoever-takes-over is reading the code at 11pm. Eight weeks, one engineer plus AI agents, no rewrite. That was the brief.
The five ideas that made it work
A design system is not a component library. We kept reminding ourselves of this throughout the sprint, because there is enormous gravity toward “Storybook full of widgets” as the deliverable. The widgets are the easy part. The five ideas below are what actually distinguishes a system that holds from a folder of .astro files.
Tokens, not values
Every spacing, radius, shadow, z-index and color in src/ reads from a --ds-* custom property. The codemod replaced 8,814 individual literals across 499 files; the CI lint blocks new ones. Magic numbers in components are a bug class, not a style preference. When a designer asks “can we make this 6px bigger?”, the answer is “we adjust the token; the 47 places it appears adjust with it.”
/* 4pt grid, 15 steps. Composable, not arbitrary. */
:root {
--ds-space-0: 0;
--ds-space-0_5: 2px;
--ds-space-1: 4px;
--ds-space-2: 8px;
--ds-space-3: 12px;
--ds-space-4: 16px;
--ds-space-5: 20px;
--ds-space-6: 24px;
--ds-space-8: 32px;
--ds-space-10: 40px;
--ds-space-12: 48px;
--ds-space-16: 64px;
--ds-space-20: 80px;
--ds-space-24: 96px;
--ds-space-32: 128px;
} Hub-tinted identity
YPAI’s blog has six content hubs — compliance, infrastructure, data engineering, agentic AI, industry solutions, research — and each one carries its own accent color. The accent is exposed as --hub-accent and the design system reads it via --ds-color-accent, which means a single <Button> component renders violet on a compliance page and teal on a research page without conditional code. Color follows content. The component does not need to know which hub it is in; the cascade tells it.
Reading-surface vs cockpit
A design system has to know what kind of page it is decorating. Article pages are reading surfaces: one column, generous line-height, footnotes inline, no chrome competing with the prose. Dashboards and admin pages are cockpits: dense, grid-aligned, status-color-rich, latency-honest. Most design systems treat these as a single grammar with options. We treat them as two grammars with shared atoms.
| Property | What most DS do | What we did |
|---|---|---|
| Token layer | Variants per component | Single --ds-* layer, codemod-migrated |
| Accent color | Brand color, period | Hub-tinted; component reads --hub-accent |
| Page grammar | One layout, options bolted on | Reading-surface vs cockpit — two grammars, shared atoms |
| Motion | Animate everything; turn off via media query | data-motion="full|subtle|none" + reduced-motion contract |
| AI / search | Robots.txt and pray | schema.org Dataset + llms.txt + /article-md/ + Pagefind + OG |
Motion that earns its keep
Motion is the easiest thing to get wrong because it feels free. We expose three modes via a single attribute and treat the OS-level reduced-motion preference as a contract, not an afterthought 2 Footnote 2The data-motion contract is documented at /design/motion/. The TL;DR: none disables decorative motion, subtle keeps functional transitions (focus rings, dialog enter/exit) but no decorative reveals, full is the default. prefers-reduced-motion: reduce globally rewires every --ds-motion-duration-* token to 0s — components do not need per-element opt-ins. The data-motion contract is documented at /design/motion/. The TL;DR: none disables decorative motion, subtle keeps functional transitions (focus rings, dialog enter/exit) but no decorative reveals, full is the default. prefers-reduced-motion: reduce globally rewires every --ds-motion-duration-* token to 0s — components do not need per-element opt-ins..
---
// Decorative reveal. Honors the inherited data-motion contract.
interface Props { delay?: number }
const { delay = 0 } = Astro.props;
---
<div
class="ds-reveal"
data-motion="full"
style={`--reveal-delay: ${delay}ms`}
>
<slot />
</div>
<style>
/* prefers-reduced-motion rewires --ds-motion-duration-base to 0s
globally. No per-component fallback needed. */
.ds-reveal {
opacity: 0;
transform: translateY(8px);
transition:
opacity var(--ds-motion-duration-base) var(--ds-motion-easing-decel),
transform var(--ds-motion-duration-base) var(--ds-motion-easing-decel);
transition-delay: var(--reveal-delay);
}
[data-motion='none'] .ds-reveal,
[data-motion='none'].ds-reveal {
transition: none;
opacity: 1;
transform: none;
}
</style> <!-- Set once at the root. The cascade does the rest. -->
<html data-motion="full">
<!-- ...components below inherit "full" unless they opt down... -->
<article data-motion="subtle"><!-- decorative reveals off, focus rings on --></article>
<aside data-motion="none" ><!-- no transitions at all --></aside>
</html>
<!-- OS preference rewires the duration tokens globally,
independent of the data-motion value declared in markup. -->
@media (prefers-reduced-motion: reduce) {
:root {
--ds-motion-duration-instant: 0s;
--ds-motion-duration-fast: 0s;
--ds-motion-duration-base: 0s;
--ds-motion-duration-slow: 0s;
}
} AI-citable content layer
A design system in 2026 is not done when it looks good in Chrome. It has to be legible to the systems that quote you back to your prospective customer. Every article on yourpersonalai.net ships with schema.org/Dataset JSON-LD, a /llms.txt route summarising the canonical pages, a /article-md/<slug>/ Markdown twin for AI ingestion, a Pagefind index that runs client-side with no API key, and per-route OG images generated at build 3 Footnote 3We chose Pagefind over Algolia for three reasons: it’s static (no API key in the client bundle), it costs nothing (Algolia’s free tier rate-limits aggressively at our traffic), and the quality is good enough that we cannot tell the difference at 83 blog posts. The “good enough” line will move; we’ll revisit at ~500 posts. We chose Pagefind over Algolia for three reasons: it’s static (no API key in the client bundle), it costs nothing (Algolia’s free tier rate-limits aggressively at our traffic), and the quality is good enough that we cannot tell the difference at 83 blog posts. The “good enough” line will move; we’ll revisit at ~500 posts.. The design system extends to how AI systems read us, not just how humans see us.
What’s actually shipped
The reference site at /design/ is now public. Concretely:
/design/— overview + the four principles, with live token previews/design/tokens/— every--ds-*token rendered as a swatch or spec/design/motion/— duration/easing scale + thedata-motioncontract/design/components/— index of all 9 primitives/design/components/icon/—lucide-static-backed, tree-shaken/design/components/input/— text input + validation states/design/components/select/— accessible custom select with keyboard nav/design/components/tabs/— including thedata-tab-panelworkaround for dynamic slots/design/components/tooltip/— Floating UI positioning, ARIA correct/design/components/dialog/—<dialog>element, focus trap, restore-focus on close/design/components/toast/— token-aware z-index, motion-respecting/design/components/skeleton/— shimmer that disappears under reduced-motion/design/components/footnote/— the component you’re reading right now/design/principles/— long-form articles on the five ideas above/design/changelog/— semver-locked release history, ending with v1.0.0 today
Every component page ships a live preview, a copy-paste code block, a do/don’t list, an anatomy diagram, and an accessibility note. The component pages are themselves rendered through the design system, which is the strongest possible smoke test.
What surprised us during the sprint
The --radius-md collision was three years old. Three different files declared it: 8px, 12px, 16px. The “winner” on any given page was whichever stylesheet the route bundler loaded last. Cards on the marketing pages used 8, cards in the freelancer portal used 16, cards on the blog used 12, and nobody had noticed because nobody loaded all three routes back-to-back. The codemod that consolidated these to a single --ds-radius-md: 12px shipped 8,814 token replacements; we visually diffed every changed page in Playwright before merging.
Astro doesn’t allow dynamic slot names. This bit us building the Tabs primitive: we wanted <Tab name="Overview">…</Tab> to render into a slot named "Overview" on the parent <Tabs>. Astro’s slot system is static at compile time, so this is rejected with a useful but firm error 4 Footnote 4Astro slot names must be string literals at compile time — they cannot be expressions. This is well-documented at docs.astro.build/en/basics/astro-components/#named-slots and is by design (server-side slot routing is one of the things that keeps Astro’s hydration story simple). The workaround pattern we landed on — data-tab-panel="<name>" as a sibling attribute, with a small client script wiring panels to triggers — is documented on /design/components/tabs/. Astro slot names must be string literals at compile time — they cannot be expressions. This is well-documented at docs.astro.build/en/basics/astro-components/#named-slots and is by design (server-side slot routing is one of the things that keeps Astro’s hydration story simple). The workaround pattern we landed on — data-tab-panel="<name>" as a sibling attribute, with a small client script wiring panels to triggers — is documented on /design/components/tabs/.. Our workaround: each tab panel writes a data-tab-panel="<name>" attribute on its outer wrapper, the Tabs root collects them via querySelectorAll, and the tab list mounts a small client script that toggles [hidden] on the matching panel. Total client JS for this: 1.4KB.
Cascade order beats @layer. Modern CSS has @layer for exactly the problem of “I want my tokens to load first so my legacy files can override only intentionally.” We set it up, expected it to work, and watched legacy unlayered styles continue to beat layered ones — because unlayered styles win over layered ones by spec 5 Footnote 5The CSS cascade order treats unlayered styles as “more important” than any @layer, which is the opposite of what most developers expect on first read. See MDN: Cascade layers for the canonical explanation. We ended up using @layer for intra-system precedence only and ordering @import statements for cross-system precedence. The CSS cascade order treats unlayered styles as “more important” than any @layer, which is the opposite of what most developers expect on first read. See MDN: Cascade layers for the canonical explanation. We ended up using @layer for intra-system precedence only and ordering @import statements for cross-system precedence.. We kept @layer for intra-system precedence (resets vs primitives vs utilities) and reordered the actual @import statements in global.css for cross-system precedence. Sometimes the right answer is the boring one.
The “WIP from another session blocks build” pattern is recurring. Multiple AI agents working in parallel on the same repo will occasionally leave a half-written file in the working tree; the next npm run build fails on a parse error in a file nobody on the current session touched. We inverted the recipe twice during this sprint: first by adding a pre-build script that warns about uncommitted .astro files, then by adding a stash-and-restore wrapper inside deploy.sh. The second version is the one that survived. We are still iterating on this; expect a follow-up post.
What we deferred to v1.1
Where to look
- The reference site: /design/
- The five principles, long-form: /design/principles/
- The release history: /design/changelog/
- The GitHub repo: currently private; opening publicly alongside v1.1. Until then, the reference site is the canonical source.
If you find a bug, an a11y regression, or a token that should exist but doesn’t, the fastest way to reach us is [email protected]. We will open a public issue tracker when the repo flips.
We built this for ourselves. We are publishing it because the conversations we needed to have during the sprint — about cascade order, about hub identity, about reading-surface ergonomics, about what “reduced motion” should mean as a contract — were conversations the rest of the design-system world is also having. If any of the five ideas above lands, take them. None of them are ours.