Skip to content

Migrate from Loki

Loki renders each Storybook story in headless Chrome (Docker, local, or Lambda — plus iOS/Android simulators) and pixel-diffs the screenshots.

Loki is alive, but pre-1.0 and Storybook-locked

Loki is still maintained (latest v0.35.1, Aug 2025), but it's single-maintainer, pre-1.0, and slow to track Storybook majors — and it's Storybook-only: it can't test full application pages or live URLs, and there's no central review UI (file-based baselines + a local loki approve). If you've outgrown Storybook-only testing, or want hosted review, Dungbeetle is a maintained step up.

Why Dungbeetle

LokiDungbeetle
ScopeStorybook stories onlyAny URL — stories and full pages, terminal, desktop, API, perf, games
DiffPixelStructured tree (+ tolerant pixel fallback)
EngineChrome (Docker/Lambda/local) + mobile simsPlaywright/Chromium
Central reviewNo (.loki/ PNGs + CLI approve)Yesself-host or managed
Release cadencePre-1.0, slowActive

Mobile simulators

Loki can screenshot stories in iOS/Android simulators (React Native). Dungbeetle's web capture is Chromium-based and doesn't drive device simulators — if simulator screenshots are core to your suite, that part won't map. Web/Storybook stories migrate cleanly.

Point Dungbeetle at your stories

Loki discovers stories from Storybook automatically; Dungbeetle captures URLs, so you target each story's static iframe URL. Build Storybook, then list the stories you want as web targets:

sh
npm run build-storybook   # outputs storybook-static/
npx http-server storybook-static -p 6006   # or any static server

A story's iframe URL is /iframe.html?id=<story-id> (the id from the Storybook URL's ?path=/story/<id>):

json
// dungbeetle.config.json
{
  "version": 1,
  "project": { "name": "design-system" },
  "lifecycle": {
    "start": ["npx http-server storybook-static -p 6006"],
    "wait": { "url": "http://localhost:6006", "timeoutMs": 30000 },
    "capture": [
      { "kind": "web", "name": "button--primary",
        "driver": "playwright", "screenshot": true,
        "url": "http://localhost:6006/iframe.html?id=button--primary",
        "viewport": { "width": 1366, "height": 768 } },
      { "kind": "web", "name": "button--disabled",
        "driver": "playwright", "screenshot": true,
        "url": "http://localhost:6006/iframe.html?id=button--disabled",
        "viewport": { "width": 1366, "height": 768 } }
    ]
  }
}

Stories also have structure

With driver: "playwright" and screenshot: true you match Loki's pixel behaviour. But the story iframe is also DOM — drop screenshot and Dungbeetle diffs the structured component markup, which is far less flaky than pixel diffs for most components. Use pixels where rendering fidelity matters, structure elsewhere.

Field mapping (Loki config lives under a loki key in package.json):

LokiDungbeetle
story (auto-discovered)one web target per story iframe URL
configurations[].width / heightcapture[].viewport
configurations[].preset (e.g. "iPhone 7")capture[].viewport (set width/height)
configurations[].target (chrome.docker, …)driver: "playwright" + browser
chromeSelectorcapture the iframe (the story root); use lifecycle.wait for readiness
diffingEngine / thresholdcomparison.pixelTolerance
.loki/reference/dungbeetle.snapshots/

Map the workflow

LokiDungbeetle
loki updatedungbeetle update
loki testdungbeetle test (or dungbeetle ci)
loki approvere-run dungbeetle update, or promote in the cloud review UI
sh
dungbeetle update                                   # ≈ loki update → dungbeetle.snapshots/
dungbeetle test                                     # ≈ loki test
dungbeetle ci --json report.json --html report.html # in CI

Commit dungbeetle.snapshots/; the .loki/reference PNGs don't carry over.

Add hosted review

Loki has no shared review surface. Push to a Dungbeetle cloud server for hosted baselines, a review/approve/promote UI, and flakiness analytics:

sh
dungbeetle push --report report.json \
  --server "$DUNGBEETLE_SERVER_URL" \
  --client-id "$DUNGBEETLE_CLIENT_ID" --client-secret "$DUNGBEETLE_CLIENT_SECRET"

Next

Source-available: CLI under FSL-1.1-ALv2, cloud server under BUSL-1.1. See Licensing.