Getting Storybook Working with Deno 2.6
The Deno team's tracking issue for Storybook support (denoland/deno#20610) has been open since September 2023, and most of the community discussion around it references Deno 2.0-era failures — panics, module resolution crashes, and ESM/CJS interop breakage. But Deno has moved fast since then. On Deno 2.6.8 with nodeModulesDir: "auto", Storybook 8 boots, serves, and renders components. Here's exactly what works, what doesn't, and how to handle the gaps.
The Quick Version
Storybook 8.6 runs under Deno 2.6. Storybook 10 does not — it checks process.versions.node and rejects Deno's reported version (v20.11.0, needs 20.19+). Use .mjs config files instead of .ts. Install framework packages explicitly. Wire up a Vite plugin to resolve workspace imports. Use relative filesystem paths for CSS @import. Enable the automatic JSX runtime. Five workarounds, all straightforward.
What Actually Works
Running deno run -A npm:storybook@8 dev starts the Storybook dev server with both the manager and preview compiling successfully. In our testing, the manager built in 97ms and the preview in 463ms — comparable to Node performance. The Vite builder, React framework, and hot module replacement all function correctly.
The CLI itself works too:
deno run -A npm:storybook@8 --version
# 8.6.15
No special flags, no patches, no forks. Just deno run -A npm:storybook@8.
The Five Workarounds
1. Use .mjs configs, not .ts
Storybook uses esbuild-register to load TypeScript config files (.storybook/main.ts). This loader has a compatibility issue with Deno's Node compat layer — specifically, it tries to read a property that's undefined in Deno, causing a TypeError: Cannot read properties of undefined (reading 'includes').
The fix is simple: use .mjs instead of .ts for your Storybook config files.
// .storybook/main.mjs
/** @type {import('@storybook/react-vite').StorybookConfig} */
const config = {
stories: ["../stories/**/*.stories.@(ts|tsx)"],
framework: "@storybook/react-vite",
addons: [],
};
export default config;
You still get type hints via the JSDoc @type annotation. The only thing you lose is direct TypeScript syntax in the config file itself — your stories and components remain fully TypeScript.
2. Install framework packages explicitly
When running Storybook through deno run -A npm:storybook@8, the framework and renderer packages aren't automatically resolved as peer dependencies. You need to install them explicitly:
deno install npm:@storybook/react@8 npm:@storybook/react-vite@8
Without @storybook/react, you'll get a Vite error: Failed to resolve import "@storybook/react/dist/entry-preview.mjs". Both packages need to be in your dependency graph for the preview to compile.
3. Resolve workspace imports with a Vite plugin
This is the big one, and it's not Storybook-specific — it's a Deno workspace limitation. Deno doesn't create node_modules/@workspace/ symlinks (denoland/deno#27550), so Vite can't resolve @workspace/* imports out of the box.
If you're already running Astro or another Vite-based app in your Deno workspace, you've likely already built a resolver plugin. The same plugin works for Storybook via the viteFinal config option:
// .storybook/main.mjs
/** @type {import('@storybook/react-vite').StorybookConfig} */
const config = {
stories: ["../stories/**/*.stories.@(ts|tsx)"],
framework: "@storybook/react-vite",
async viteFinal(config) {
config.plugins = config.plugins || [];
config.plugins.push(denoWorkspaceResolver());
return config;
},
};
The denoWorkspaceResolver plugin reads each workspace member's deno.json exports map and creates Vite aliases that point to the actual file paths. This is the same approach used for Astro integration — the plugin is portable across any Vite-based consumer.
4. Use relative filesystem paths for CSS @import
PostCSS resolves @import via Node's filesystem, not Deno's import map. This means @import "@workspace/tailwind/globals.css" will fail with ENOENT even though the same specifier works in TypeScript files.
Use relative paths instead:
/* styles/globals.css */
@import "../../../packages/tailwind/src/globals.css";
@source "../../../packages/ui/src/**/*.{ts,tsx}";
@source "../stories/**/*.{ts,tsx}";
This is consistent with how Astro and Next.js handle it in the same workspace — CSS imports always use relative filesystem paths.
5. Enable the automatic JSX runtime
If your components use import type * as React from "react" (type-only imports, as shadcn/ui does), they rely on the automatic JSX transform to inject the runtime. Vite's default esbuild config may not enable this, causing a Can't find variable: React error at render time.
Add this to your viteFinal config:
async viteFinal(config) {
config.esbuild = config.esbuild || {};
config.esbuild.jsx = 'automatic';
// ... other config
return config;
}
What Doesn't Work
Storybook 10 fails immediately with a version gate: "To run Storybook, you need Node.js version 20.19+ or 22.12+." Deno 2.6's compat layer reports as Node v20.11.0. This will likely resolve as Deno updates its internal Node version reporting, or Storybook could be convinced to relax the check.
TypeScript config files fail due to esbuild-register incompatibility. This is a third-party loader issue, not a Deno or Storybook issue. Using .mjs is a clean workaround with no functional loss.
Project Structure
In a Deno workspace monorepo, Storybook belongs in apps/storybook — not inside the UI package. It's a consumer of @workspace/ui, @workspace/fonts, and @workspace/tailwind, which makes it an app-layer concern:
apps/
storybook/
.storybook/
main.mjs
preview.mjs
stories/ # Optional — stories can also live in packages/ui/src/
deno.json
The deno.json for the Storybook app is minimal:
{
"tasks": {
"dev": "deno run -A npm:storybook@8 dev -p 6006",
"build": "deno run -A npm:storybook@8 build"
}
}
Tailwind CSS v4 Integration
Storybook needs the same Tailwind setup as your other apps. Create a local styles/globals.css that imports your shared Tailwind config using a relative filesystem path (see workaround #4) and defines @source directives pointing at the UI component directory:
@import "../../../packages/tailwind/src/globals.css";
@source "../../../packages/ui/src/**/*.{ts,tsx}";
@source "../stories/**/*.{ts,tsx}";
Import this from .storybook/preview.mjs and Storybook renders components with the same styles as your production apps.
Bottom Line
The Storybook-on-Deno story has quietly gone from "completely broken" to "works with five known workarounds." The core Vite-based build loop runs correctly. The friction points are all at the edges — config file loading, dependency resolution, workspace imports, CSS paths, and JSX runtime — and each has a straightforward fix. If you're already running Vite-based apps in a Deno workspace, adding Storybook is incremental, not architectural.