Journey
MilestoneDesign SystemsadoptionReact

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 /article pager: 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 hreflang links, real /article/page/2 URLs) — but the package rendered plain buttons. We taught the packages to render links (a renderLink hook) so they stay SPA-navigable and crawlable at once.
  • Localized status: the pager hardcoded "Page X of Y"; we added a status prop so /id reads "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 learned this week

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

  1. ui.pangaea.id — the Pangaea design system catalog
  2. labspangaea/pangaea.ui — the repository