Behind the Scenes

We Turned Storybook Off: One /_ui Route So Designers, Engineers, and Clients See the Same Truth

2026.09.18 · 12 views
We Turned Storybook Off: One /_ui Route So Designers, Engineers, and Clients See the Same Truth

No separate component-library site. Instead: a real Blade + Tailwind gallery route inside the project, one-way Figma Variables sync into tokens.json, and Playwright screenshot diffing as the gate — plus the honest costs.

Share:

1. What went wrong before we had this

Two years ago we built an admin panel for a retail client, with the component library in a standalone Storybook site. Two weeks before launch the designer and the engineer had both signed off, and then the client said: "This doesn't look like what you showed me." The cause: Storybook had its own slimmed-down CSS entry point and never loaded the form resets the project added later. The result:

  • Stories were missing four variants that existed in the real Blade components, and nobody backfilled them.
  • The designer measured against Storybook, the engineer built against the project, and reconciling them burned three days.
  • The client saw a third version of the truth on staging.

We didn't lose to a tool. We lost to having one more thing that needed syncing.

2. What we do now

In one sentence: the component library is not a separate site, it is a route inside the project.

Every Laravel project ships a /_ui route using the same layout, the same compiled Tailwind output and the same Blade components as production, rendering every component state onto one page. All three parties open the same URL.

The pipeline:

Figma Variables (single source of truth) → tokens.json (one-way, never written back) → Tailwind @theme variables → Blade components consume only Tailwind classes → /_ui renders the real components → Playwright screenshot diffing → GitHub Actions gates the PR → client reviews the same URL

The key property: nothing in that chain is a copy that exists purely for display. If /_ui is broken, production is broken too.

3. Why we built it this way

There is nothing wrong with Storybook. It solves developing components in isolation from the application, and for organisations with a dedicated frontend team and components shared across products, its own documentation makes a clear case.

Our shape is different: a studio of three to six people, each project a self-contained Laravel app, components almost never shared across engagements. The trade-offs:

  • Gave up isolated development, bought zero drift. The real layout is more susceptible to outer-layer styles, but "correct in the showcase, wrong in production" can never happen.
  • Gave up auto-generated props tables, bought clickable real states. Clients don't read props tables; they want to see a disabled button and a failed validation.
  • Gave up two-way sync for strict one-way sync. Figma owns colour, spacing and type scale; code only consumes. Two-way sync needs someone on duty maintaining it.

4. How we actually do it

Step 1: build token collections in Figma Variables — colour, spacing, radius, type scale — named to match Tailwind namespaces, using modes for light and dark.

Step 2: export via the Figma REST API variables endpoint and commit to the repo.

design/tokens.json
resources/css/app.css
resources/views/components/          # the Blade components
resources/views/_ui/index.blade.php  # the gallery page
tests/visual/ui-gallery.spec.ts
{
  "color": {
    "brand": { "500": { "value": "#F59E0B" }, "600": { "value": "#D97706" } }
  },
  "radius": { "card": { "value": "12px" } },
  "space":  { "gutter": { "value": "24px" } }
}

Step 3: compile to CSS variables with Style Dictionary and feed Tailwind's @theme, so each token becomes a usable utility class.

@import "tailwindcss";
@theme {
  --color-brand-500: #F59E0B;
  --radius-card: 12px;
}

Step 4: write the /_ui gallery, extending the same layouts/app.blade.php production uses and listing every state of every component. The route is exposed only outside production, or through a signed link.

Step 5: run visual regression with Playwright's toHaveScreenshot(); the first run generates the baselines. The same page also gets an axe scan, with contrast checked against the AA threshold in WCAG 2.2.

test('ui gallery visual', async ({ page }) => {
  await page.goto('/_ui');
  await expect(page).toHaveScreenshot('ui-gallery.png', {
    maxDiffPixels: 120, fullPage: true,
  });
});

Step 6: GitHub Actions runs the suite on PR open and posts any diff back as an image. Intentional changes get -u to update the baseline, which then goes through code review with the code.

5. What this approach costs us

  • Components can't be developed in isolation. To look at one component you must boot the whole project.
  • Visual regression produces plenty of false alarms. Font loading timing, animation residue and OS font rendering all create pixel diffs. Tuning maxDiffPixels took about two weeks, during which engineers started reflexively ignoring red builds.
  • No auto-generated API documentation. Blade component props are documented by hand. Our PR template requires it, and we still miss some.
  • Cross-project reuse gets harder. Carrying a component forward means copy and adapt, not npm install.

6. Where this is the wrong choice

  • When the library serves multiple products or teams. Once components need version numbers, changelogs and downstream subscribers, go back to Storybook plus a published package.
  • Pure frontend SPAs. Our premise is server-rendered Blade; React and Vue components are already decoupled from routes, so Storybook's isolation advantage grows.
  • When designers need to tweak parameters live. Storybook's controls panel is genuinely useful; we only have static states.

7. Why clients should care

Quality: the button you approve at sign-off is the button that ships — corner radius, spacing and hover all from the same compiled CSS as production.

Timeline: the old "this doesn't match the design" dispute cost three to five working days. With one shared URL, the conversation shifts from "whose version counts" to "do we want to change it".

Cost: we no longer bill for building and maintaining a component library; it is part of the project. In exchange, our quotes list the hours for establishing baselines — a one-off 8 to 12 hours.

8. If you want to try this

  • First confirm your components genuinely aren't shared across projects. If they are, this approach bites you by the third project.
  • Keep token sync strictly one-way. Code never renames a token — change it in Figma and re-export.
  • Start visual regression with three critical components. Our order was buttons, then form fields, then cards, with two stable weeks between expansions.
  • Add one line to your PR template: "Does this change touch /_ui?" Without that reminder, the gallery goes stale within three months.

A pre-adoption checklist:

  • ☐ Are you certain components never need versioning across projects?
  • ☐ Is the Figma side already on Variables rather than scattered styles?
  • ☐ Can CI render screenshots consistently?

9. Where we've used it

A reservation system for a northern Taiwan restaurant chain: twenty-three Blade components, with /_ui showing all six states of the table-status card. The client's marketing lead opened the URL during acceptance and worked through it item by item, collapsing two review meetings into one.

A quoting back-office for a B2B industrial parts distributor: the designer worked for the client, not us. One-way token sync let her change the brand colour in Figma; we re-exported and the whole site recoloured within half an hour.

10. Frequently asked questions

So you don't recommend Storybook at all?

Not at all. With a dedicated frontend team, components shared across products, or a pure React/Vue project, Storybook is still what we'd suggest.

Doesn't visual regression eat a lot of CI time?

A full-page diff runs in roughly 40 to 90 seconds on GitHub Actions. The cost is maintaining baselines, not runtime.

Our designer doesn't code. How does she participate?

Two things: maintain Variables in Figma, and open /_ui to see the result. She never touches compilation or deployment.

Can clients actually understand /_ui?

Better than you'd expect. A plain-language note at the top explains that this is a component catalogue rather than a real page, and clients usually start opening it unprompted by the second meeting.

Won't the gallery become an unmaintained orphan?

It will, without a mechanism. We rely on the PR template reminder and a failing visual regression to force it back up to date.

11. Want to talk this through?

If you're weighing up a design system, or your component library has drifted so far from production that nobody dares open it, we're happy to look at your project shape and say whether this approach fits — including saying plainly when it doesn't.

And if you're the kind of engineer or designer who will argue for half an hour about whether a particular diff counts as a bug, we're hiring. ScriptWalker's stack is Laravel, Flutter and Tailwind — send us your work, or the thing you most want to criticise about how we do this.

Share:
Behind the Scenes Back to Blog