You ask Rork to add an out-of-stock badge to the cart screen one evening, and the next morning you notice the product detail page has shifted by exactly two pixels in the padding. I have run into that exact situation more times than I want to admit while building a Rork-powered ecommerce app. The single file the AI rewrote was tied through a shared component to a screen on the other side of the app, and the change quietly traveled with it.
The hard part is not that things change — it's that you stop noticing when they change. Walking through every screen in a simulator does not scale, and reviewing staging screenshots by eye breaks down somewhere around the tenth screen. This is where Storybook for React Native, paired with Chromatic, earns its keep as a visual regression safety net for Rork projects.
This guide walks through everything I wish someone had handed me when I started: how to retrofit Storybook and Chromatic onto a Rork-generated codebase, the AI-specific traps that will steal a day if you don't see them coming, and the team workflow that turns "diff review → approve → merge" into a habit. By the end you'll have a system that lets you accept Rork's proposals with eyes open instead of crossed fingers.
Why Visual Regression Tests Pay Off Especially Well on Rork
Visual regression testing started life as a discipline for design system teams. But for a solo developer leaning on Rork, the payoff is even sharper. Three reasons stand out.
First, Rork makes cross-file changes from a single prompt. Saying "unify the corner radius on buttons" can quietly touch Button.tsx, Card.tsx, and Modal.tsx in one shot. When the diff sprawls, eyeball review is going to miss things — that is just arithmetic.
Second, AI tends to "tidy up" code beyond what you asked for. Sometimes that's a gift, sometimes it's a problem. Either way, three months later you will not remember why a particular spacing changed. Visual regression turns those silent tidying passes into something you have to consciously approve or reject.
Third, Rork makes no guarantees about the stability of the code it produces. That is not a complaint, it's a fact of life with AI tooling — the underlying model evolves and so does the code style. The pragmatic response is to lock the visual end state on the human side, as a kind of contract. The stories you write in Storybook are exactly that: a quiet declaration of "this is how this component is supposed to look."
I shipped three Rork apps without this safety net before adding it, and in all three I had moments after launch where I thought, "wait, did this screen always look like this?" Treat visual regression tests as insurance for the AI age.
The Storybook for React Native Landscape (April 2026)
Before you install anything, it helps to know what you're choosing between. As of April 2026 there are roughly three options.
@storybook/react-native is the official package for running stories on a real device. It's great for verifying touch behavior and native modules, but driving it from CI takes effort, and uploading to Chromatic from on-device builds is awkward.
@storybook/react-native-web-vite renders your stories through React Native Web in the browser. Chromatic is built around web Storybook, so this combination is by far the easiest path to visual regression testing. It is what I use in production today.
Storybook 9 is starting to bring both worlds under a single configuration, but in my experience the safest move right now is to commit fully to the web side and not try to dual-target until you actually need to.
You may be wondering: what about bugs that only show up on a real device — touch targets, gestures, native modules? Those are not in the visual regression team's job description; they belong to your end-to-end suite, which I covered separately in the Detox/Maestro E2E guide. Visual regression watches color, spacing, layout, and typography. Pinning that scope down is what keeps the system from sprawling.
Step 1: Retrofit Storybook Onto an Existing Rork Project
Notice the word "retrofit." The fun starts when you take a Rork-generated Expo project that already has dozens of components and bolt Storybook on with the smallest possible footprint. The instructions assume Expo SDK 53 or later, React Native 0.79 or later, and Node 20.
# Install everything in one pass
npx expo install react-native-web react-dom @expo/metro-runtime
npm install --save-dev \
storybook@^9 \
@storybook/react-native-web-vite@^9 \
@storybook/addon-essentials@^9 \
@storybook/addon-interactions@^9 \
@vitejs/plugin-react@^4 \
vite@^5
# Generate Storybook config (Vite + React Native Web)
npx storybook@latest init --type react-native-web
# Expected output:
# ✔ Storybook configuration files were created in ./.storybook
# ✔ Run npm run storybook to start itThe --type react-native-web flag is the part most people miss. If you let the CLI default to a plain React template, imports from react-native won't resolve and the build collapses. I lost an entire evening to that exact misstep.
Once the files are generated, I always patch .storybook/main.ts to alias react-native to react-native-web explicitly and to pin the extension order:
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-native-web-vite";
import { mergeConfig } from "vite";
const config: StorybookConfig = {
stories: [
"../components/**/*.stories.@(ts|tsx)",
"../app/**/*.stories.@(ts|tsx)",
],
addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
framework: {
name: "@storybook/react-native-web-vite",
options: {},
},
// Absorbs the absolute aliases (@/components/...) Rork tends to emit
viteFinal: async (config) =>
mergeConfig(config, {
resolve: {
alias: {
"@": new URL("../", import.meta.url).pathname,
},
// The extension order matters more than it looks. If you don't put
// .web.tsx first, Platform.select branches resolve incorrectly and
// some views render blank in the browser.
extensions: [".web.tsx", ".web.ts", ".tsx", ".ts", ".jsx", ".js"],
},
}),
};
export default config;I burned thirty minutes on the extensions order before I caught it. If a story is mysteriously empty in the browser despite working on device, look here first.
Step 2: Write Your First Story (Treat the Component Like a Contract)
With Storybook running, pick the first component to "freeze" — the one whose appearance you would most regret breaking — and write its story. I always start at the atom layer: Button, Card, PriceTag. Anything that gets reused inside other screens is high leverage.
// components/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
title: "Atoms/Button",
component: Button,
// When Rork eventually rewrites this to add a `loading` state,
// a snapshot is taken for every story below — that's how you'll
// catch regressions you didn't ask for.
args: {
label: "Buy now",
onPress: () => {},
},
parameters: {
// Tell Chromatic to capture multiple viewports
chromatic: { viewports: [375, 414, 768] },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: "primary" },
};
export const Disabled: Story = {
args: { variant: "primary", disabled: true },
};
export const Destructive: Story = {
args: { label: "Delete", variant: "destructive" },
};
// Expected appearance:
// - Primary: brand background, white text
// - Disabled: 50% opacity, pointer events disabled
// - Destructive: red background, white textA practical heuristic from running this on real Rork projects: I cap a single component at "three to seven stories." If I find myself wanting an eighth, that's a strong hint the component should be split into two. Keep that discipline early and the story library stays maintainable.
Step 3: Wire Up Chromatic for Visual Diffing
Once npm run storybook runs locally, hooking up Chromatic is the fast part. Chromatic's free tier covers 5,000 snapshots per month, which lasts a long time for an indie project.
# Install the Chromatic CLI
npm install --save-dev chromatic
# Create a project at chromatic.com and grab the token
export CHROMATIC_PROJECT_TOKEN="chpt_xxxxxxxxxxxxxxxx"
# First upload — establishes the baseline on main
npx chromatic --project-token="$CHROMATIC_PROJECT_TOKEN" \
--build-script-name=build-storybook \
--exit-zero-on-changes
# Expected output (excerpt):
# Build 1 published.
# View it online at https://www.chromatic.com/build?appId=...
# 12 stories captured ← number of snapshots takenI keep --exit-zero-on-changes on for the very first build so the CI doesn't go red on a baseline that has nothing to compare against. From the second build onward I drop it, so any unexpected diff lights up the pipeline.
Step 4: Let GitHub Actions Surface a Diff Every Time Rork Edits a File
If your workflow involves Rork Companion opening PRs (or you push branches manually), wiring Chromatic into GitHub Actions is what turns this from "an extra step you forget" into "automatic feedback before merge."
# .github/workflows/chromatic.yml
name: Chromatic
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
visual-regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Chromatic needs the full history to compute diffs against
# the baseline. Forgetting fetch-depth: 0 produces a
# "no baseline found" error that's hard to read.
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Run Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
# Block merges with unreviewed changes by failing the PR
exitZeroOnChanges: false
# Only re-snapshot stories that depend on changed files
onlyChanged: true
# Drop the Chromatic review URL into the PR on failure so
# reviewers can jump straight there.
- name: Comment Chromatic URL on PR
if: failure() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Please review the visual regression diffs: ${{ steps.chromatic.outputs.url }}',
});
# What this gives you:
# 1. Open a PR -> Chromatic builds a new snapshot set
# 2. It compares against the baseline, fails the PR if anything changed
# 3. Reviewer approves the diffs in Chromatic, CI flips to green
# 4. PR can merge to mainonlyChanged: true is the line that makes this affordable for an indie. Rork tends to produce many small PRs, and only re-snapshotting the affected stories keeps me well under 1,000 snapshots a month while still getting near-real-time feedback.
Step 5: A Review Workflow Built for AI-Generated UI
Up to here we've installed and wired things. The actual leverage shows up in the workflow you put around it. The goal is to never silently absorb whatever Rork decided to change.
I keep this checklist in my pull request template:
<!-- .github/pull_request_template.md -->
## Visual regression checks
- [ ] Chromatic build is green
- [ ] All intentional diffs were approved in Chromatic
(drop a one-line reason in the comment)
- [ ] Unintentional diffs were removed by re-prompting Rork
- [ ] If anything under `Atoms/` changed, I opened the dependent
`Organisms/` diffs as well
## Rork prompt history
<!-- Paste the prompt(s) you sent to Rork for this PR. -->
<!-- These are the only artifact a future maintainer will have to
understand why this diff exists. -->The third box — "unintentional diffs were removed by re-prompting Rork" — is the one that pays for the entire system. If you let AI-introduced tweaks land "because they look fine," the design will drift on you in slow motion. I run a personal rule: if a single PR has three or more unintended diffs, I throw the PR away and re-prompt from scratch. It feels wasteful in the moment, but the downstream cleanup it prevents is worth it three times over.
This pairs neatly with the upstream guide on locking in design tokens before you generate UI: see Building a Design System for AI App Development. The full picture is "fix the design first, then guard it later." Visual regression alone won't save you if the design system is undefined; design system alone won't save you if there is no enforcement.
The Three Pitfalls That Will Cost You a Day
Here are the three places I most often see this setup fall over — and the fix for each.
Forgetting to load fonts in Storybook. Rork-generated components almost always assume custom fonts loaded with expo-font. If Storybook doesn't load the same fonts, Chromatic snapshots fall back to a different font and every screenshot reads as "changed" forever. Wire fonts in via useFonts in .storybook/preview.tsx and only return the story once they've loaded:
// .storybook/preview.tsx
import type { Preview } from "@storybook/react";
import { useFonts } from "expo-font";
import React from "react";
const preview: Preview = {
decorators: [
(Story) => {
const [loaded] = useFonts({
"Inter-Regular": require("../assets/fonts/Inter-Regular.ttf"),
"Inter-Bold": require("../assets/fonts/Inter-Bold.ttf"),
});
// Don't render until fonts have arrived. Skipping this
// produces flicker and a constant trickle of false diffs.
if (!loaded) return null;
return <Story />;
},
],
};
export default preview;Treating dark mode as optional. Rork produces dark-mode-aware code easily, but if your stories never flip the theme you are effectively only protecting the light variant. Use Storybook's globalTypes to expose a theme toggle and Chromatic's parameters.chromatic.modes to capture both modes in the same build.
Letting non-deterministic data into your stories. Calling new Date() or Math.random() inside a component means every snapshot will differ from the last one and you'll spend your life dismissing fake diffs. The wrong fix is parameters.chromatic.disableSnapshot: true. The right fix is to refactor the component to accept a dateProvider (or similar) prop, which Rork is happy to do for you in a follow-up prompt. Worth the half hour every time.
Two Patterns I Run in Production
Two extensions of this setup have been worth the effort.
Splitting paywall stories into their own bucket. UI on the paid path — PaywallModal, SubscriptionCard, TipButton — has an outsized impact on conversion. I keep them under Paywalls/ in Storybook so I can review their diffs with extra care. Since I started doing this, I have not had a single "oh no, the price label was misaligned for two weeks" incident.
Pairing each story file with the Rork prompt that produced it. A short header comment on the story file capturing the prompt history turns out to be the single most useful artifact when you revisit a component three months later. AI-generated UI maintenance lives or dies by whether you can find the original prompt:
// components/PaywallModal.stories.tsx
/**
* Prompt history:
* 2026-04-12: "Three plans stacked vertically: Pro, Premium,
* Premium thank-you price"
* 2026-04-20: "Add a 'limited-time' badge to the thank-you price"
*
* Read this header before editing this component. Updating without
* extending the history is a gift to nobody, least of all your future self.
*/
import type { Meta, StoryObj } from "@storybook/react";
import { PaywallModal } from "./PaywallModal";
// ...That tiny ritual is what turns a Rork-built app into something you can maintain rather than something you keep regenerating from scratch.
The honest framing for all of this: a Rork × Storybook × Chromatic setup is the ritual that keeps you in the loop on UI decisions you didn't directly make. The thrill of describing a feature in plain language and watching an app appear is real. The bill comes due when you discover later that something quietly broke. Pick the one component you would most hate to see drift, write its Button.stories.tsx, push the first Chromatic build, and call it done for today. Tomorrow's relationship with Rork will already be different.
A Closer Look: Sizing the Snapshot Budget
The first month most people run this setup, they blow through Chromatic's free tier and panic. Most of the time the cause is not "too many components" but "too many viewports times themes times locales." Multiplying those dimensions across every story is the classic mistake.
A budget I've found sane for indie Rork projects looks like this. Keep base stories at one viewport (375 px) and one theme (light). Promote a story to the full matrix only when the component is actually used in a layout that adapts. Most atoms — buttons, badges, chips — render the same regardless of viewport, so paying for three viewport snapshots per story is just lighting money on fire.
Concretely, I use a small helper in .storybook/preview.tsx that gives me a vocabulary for this:
// .storybook/preview.tsx (continued)
export const parameters = {
chromatic: {
// Default for atoms: capture the lightest possible matrix
viewports: [375],
modes: { light: { theme: "light" } },
},
};
// Use story-level parameters to opt in:
// parameters: { chromatic: { viewports: [375, 414, 768] } }
// Reserve the wide matrix for organisms and screens.That single decision tends to drop snapshot consumption by 40 to 60 percent on a typical Rork codebase. It also makes diff review faster, because you're not sifting through three viewport copies of every change.
Handling the Asynchronous Nature of Rork-Generated Screens
Rork's generated screens often start with a loading state, fetch from a backend, and only then render the "real" UI. Stories that capture only the empty loading state don't tell you much. The trick is to give Storybook a deterministic source of data via mocks.
I use Mock Service Worker (MSW) for this in stories that touch the network. The setup looks heavier than it is — five files of boilerplate, then it pays for itself the rest of the project's life. The pattern:
// components/ProductList.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { http, HttpResponse } from "msw";
import { ProductList } from "./ProductList";
const meta: Meta<typeof ProductList> = {
title: "Organisms/ProductList",
component: ProductList,
};
export default meta;
type Story = StoryObj<typeof ProductList>;
// Three deterministic states — no flaky network, no time-dependent diffs
export const Loaded: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/products", () =>
HttpResponse.json({ items: [
{ id: "1", name: "Coffee Beans", price: 1200 },
{ id: "2", name: "Espresso Cup", price: 2400 },
]})
),
],
},
},
};
export const Empty: Story = {
parameters: {
msw: {
handlers: [http.get("/api/products", () =>
HttpResponse.json({ items: [] }))],
},
},
};
export const ErrorState: Story = {
parameters: {
msw: {
handlers: [http.get("/api/products", () =>
HttpResponse.error())],
},
},
};Now your stories cover the three states the design needs to support, with no real network in play. When Rork later restructures the loading state — and it will — Chromatic shows you all three transitions, not just the happy path.
A Quiet Argument for Treating Stories as Documentation
There is a side benefit to this discipline that I rarely see written down. Once your component library has stories, the stories themselves become the most up-to-date documentation of the app. The Storybook URL — hosted by Chromatic, indexed and deep-linkable — turns into a place anyone (designers, future-you, a freelancer you might bring in) can land and immediately understand what the building blocks of the app look like.
For solo developers who occasionally hire help or hand projects off to acquirers, this matters more than it sounds. I've sold app concepts twice, and each time the question "show me the components" came up. Pointing to a Storybook URL beat any deck I could have made.
Migrating an Existing Project Without Losing a Weekend
If your Rork project has 30+ components today and you're worried about a long migration, the order I recommend is:
- Hour 1: install everything from Step 1, get
npm run storybookrunning with a single placeholder story. Don't try to write real stories yet — just confirm the build pipeline. - Hour 2-4: write stories for the five most "load-bearing" atoms (button, input, badge, card, image). Don't touch organisms yet.
- Hour 5: wire Chromatic, push the first build, accept the baseline.
- Hour 6: add the GitHub Actions workflow on a feature branch and confirm it runs green when you push.
- Day 2 onward: every time Rork touches a component you don't have a story for, write the story before you merge. Within two weeks you'll have meaningful coverage on the parts of the codebase that actually change.
The biggest mistake here is trying to write "complete" coverage in one sitting. Don't. Visual regression coverage is more useful where the code changes most, not everywhere.
What This Doesn't Catch — And What To Do About It
Visual regression is a focused tool. It does not catch:
- Layout problems that only manifest with very long real-world content (a 200-character product name)
- Animations and transitions
- Accessibility issues like color contrast or focus rings
- Native-only behavior (haptics, status bar appearance, keyboard handling)
For each of these you want a different layer. Long-content edge cases live in your story args (write a story called WithLongTitle). Animations live in interaction tests via @storybook/addon-interactions. Accessibility is a job for axe-core or Storybook's a11y addon. Native behavior is what end-to-end suites like Detox and Maestro are for.
The point is not to make Storybook do everything; it's to make Storybook do its specific thing extremely well so the rest of your testing strategy can specialize. A common confusion when you adopt visual regression is to start expecting it to catch things outside its scope. Resist that. Layer the tools.
Closing Note on Cost vs. Value
The honest answer to "is this worth the setup cost?" depends on whether you intend to keep working with Rork for more than another month. For a one-week prototype, skip all of this. For an app you intend to ship, maintain, and possibly monetize over a year or two, the time you spend now is small relative to the time you will lose later when an unannounced layout change reaches App Store review and gets flagged.
The version of me from two years ago would have said this was overengineering. The version of me writing this, after a few painful late-night incidents, sees it differently. Tools like Rork lower the bar for creating an app dramatically — but they don't change the cost of a UI bug in production. That gap is where this kind of safety net earns back its time, sometimes many times over.
Build the smallest version that works for your project this week. Watch how often the first real diff catches you off guard. From that moment on, you'll wonder how you shipped Rork apps without it.
A Final Practical Note: The First Component to Story-ify
If after reading all this you only have the energy to write one story today, make it your most-shared atomic component — almost certainly the primary Button. The reason is leverage. A typical Rork-generated app uses its primary button on perhaps fifteen to thirty screens. The day Rork rewrites that component for any reason, you want the alarm to go off immediately. One story file, three or four args, ten minutes of work, and that alarm is wired up forever. Everything beyond that is incremental — and you can add it as you encounter real diffs you wish you'd caught earlier.
That single button story has, more than once, saved me from shipping an app where the call-to-action color shifted by enough to fall below WCAG contrast thresholds. The diff was four pixels of saturation; without Chromatic I would have noticed when a user emailed me. With it, I noticed before merge. That gap, repeated across the lifetime of an app, is what this entire setup is buying you.