Eating our own design system
Last week we moved all of our UI into a home of its own:
one design systemⓘ, thirty-seven versioned packages, with a live
catalog at ui.pangaea.id. That's the showroom. But a design system you build and don't use is just a
museum. This week we did the part nobody claps for: we made the live site — pangaea.id — actually
consume it. Every rv-* primitive and component on the site now ships from @labspangaea/ds-*. And
the whole point is this: you, the visitor, can't tell.
The temptation we skipped
The catalog runs on Astro. The live site runs on React (vite-react-ssg). The tempting "clean" move:
migrate the site to Astro too, unify the stack. We didn't. The packages are peer-React ESM — the site
consumes them as ordinary dependencies, no framework change. Moving to Astro would have meant a
weeks-long, high-risk rewrite — routing, getStaticPaths, the MDX component model, and re-emitting
all the SEO/JSON-LD/hreflang — a de-indexing minefield — for zero user-visible benefit.
The rule: the live site wins
When a package and the live site disagreed on a detail, we held one rule: the live site wins — port
its look and behavior back into the package. With one exception: if the package's version was a
genuine accessibility improvement (a WCAG-safe focus ring, a 44px touch target), we keep it in the
package — and let the site adopt it.
Different because the site is more correct
- Pager pills → plain text: the site wins
- Colours, spacing,
breakpoints: match the site
Different because the package is better (WCAG)
- AA
focus ring, 44px touch target, hover gated to a real pointer - Keep it in the package; the site adopts it
Phase 1: prove it changed nothing
The foundation — fonts, tokens, the card/section/layout primitives — is pure CSS. So we adopted it
under one hard rule: prove the rendered site is byte-identical. The method: build main, build the
branch, strip the asset-hash fingerprints + the build nonce, then diff the prerendered HTML.
Identical → the SEO, the structured data, the content, and the i18n are provably untouched.
Phase 2: the components that fought back
The interactive components weren't byte-identical — they have behavior. Each one forced a decision.
- The
/articlepager: the package styled it as pill buttons; the site's was plain text links. We reverted the package to plain text — fidelity wins. - The EN|ID switcher and the pager: both are SEO-critical link controls (crawlable
hreflanglinks, real/article/page/2URLs) — but the package rendered plain buttons. We taught the packages to render links (arenderLinkhook) so they stay SPA-navigable and crawlable at once. - Localized status: the pager hardcoded "Page X of Y"; we added a
statusprop so/idreads "Halaman X dari Y". - The navbar — on every page, the riskiest — we adopted CSS-only: keep the site's own Nav component (its SPA links, active state, mobile-menu close, analytics), move only the styling into the package. Zero visible change, and as a bonus it de-duplicated the site's own redundant nav CSS.
If a control matters for SEO, it needs real links — not buttons. Teach the package to render links; don't downgrade the site to buttons.
What we'd tell ourselves
Do
- Prove it's byte-identical before you trust a CSS swap
- Let the live site win on visual fidelity
- Teach the package (not the site) when there's a real gap — SEO links, i18n status
- Adopt the risky navbar CSS-only
Don't
- Rewrite your framework just to consume a library
- Ship a "close enough" visual change to the live site
- Put an SEO-critical control on a component that can't render links
The point of it
A design system you build but don't use is just a museum. The proof it's real is that your own most
important product depends on it. The whole design layer of pangaea.id now moves when
@labspangaea/ds-* moves — and the day we did it, nobody could tell. That's the win.
How the catalog and packages are built, end to end, is in the article: Shipping a design system at ui.pangaea.id. And why we chose versioned packages over runtime federation is in the note: The federation we didn't build.
Sources