https://github.com/ftzi/nextbook
π¨ Next-gen component stories for Next.js
https://github.com/ftzi/nextbook
Last synced: 5 months ago
JSON representation
π¨ Next-gen component stories for Next.js
- Host: GitHub
- URL: https://github.com/ftzi/nextbook
- Owner: ftzi
- Created: 2025-11-29T00:37:06.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2025-12-03T04:21:36.000Z (6 months ago)
- Last Synced: 2025-12-04T19:51:59.457Z (6 months ago)
- Language: TypeScript
- Homepage: https://nextbook.dev
- Size: 942 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
Zero-config component stories for React frameworks
[](https://www.npmjs.com/package/@ftzi/storify)
[](https://www.npmjs.com/package/@ftzi/storify)
[](http://makeapullrequest.com)
Storify is a lightweight alternative to Storybook, designed for React frameworks like **Next.js** and **TanStack Start**. It uses your app's existing configuration - no separate build process, no Tailwind duplication, no webpack config.
## β¨ Features
- **Zero Dependencies** - No runtime dependencies. Just your React app.
- **Zero Config** - Uses your app's existing setup
- **Framework Agnostic** - Works with Next.js, TanStack Start, and more via router adapters
- **Path-Based Hierarchy** - Keys become sidebar structure automatically
- **Zod Controls** - Auto-generate interactive controls from Zod schemas
- **Story Matrix** - Auto-generate ALL prop combinations from Zod schemas
- **Type Safe** - Full TypeScript support with IntelliSense
- **Lazy Loading** - Stories load on-demand for fast startup
- **Background Switcher** - Toggle between default and striped backgrounds to spot component imperfections
- **AI-First** - Generates `CLAUDE.md` and `AGENTS.md` files that teach AI assistants how to write stories
- **API Mocking** - Optional MSW integration to mock API endpoints in stories
## π Quick Start
```bash
npx @ftzi/storify # or: bunx, pnpm dlx, yarn dlx, etc.
```
This scaffolds the required files in `app/ui/` and creates an example story.
Then visit `http://localhost:3000/ui` to see your stories.
Manual Setup (alternative to CLI)
### 1. Install
```bash
npm install @ftzi/storify
# or
bun add @ftzi/storify
```
### 2. Register your stories
```tsx
// app/ui/stories/index.ts
"use client";
import { createStories } from "@ftzi/storify";
export const stories = createStories({
button: () => import("./button.story"),
forms: {
input: () => import("./forms/input.story"),
select: () => import("./forms/select.story"),
},
});
```
Keys become sidebar paths: `forms.input` β `Forms > Input`
### 3. Create the layout
```tsx
// app/ui/layout.tsx
import "@/app/globals.css";
import { StorifyShell } from "@ftzi/storify";
import { NextRouterAdapter } from "@ftzi/storify/next";
import { notFound } from "next/navigation";
import { stories } from "./stories";
export default function StorifyLayout({
children,
}: {
children: React.ReactNode;
}) {
if (process.env.NODE_ENV === "production") {
notFound();
}
return (
{children}
);
}
```
> **Note:** Don't add `` or `` tags here - Next.js layouts nest, so your root layout already provides them.
### 4. Create the page
```tsx
// app/ui/[[...path]]/page.tsx
import { StoryPage } from "@ftzi/storify";
import { stories } from "../stories";
export default async function Page({
params,
}: {
params: Promise<{ path?: string[] }>;
}) {
const { path = [] } = await params;
return ;
}
```
TanStack Start Setup
### 1. Install
```bash
npm install @ftzi/storify
```
### 2. Register your stories
```tsx
// src/stories/index.ts
"use client";
import { createStories } from "@ftzi/storify";
export const stories = createStories({
button: () => import("./button.story"),
});
```
### 3. Create the layout route
```tsx
// src/routes/ui.tsx
import { StorifyShell } from "@ftzi/storify";
import { TanStackRouterAdapter } from "@ftzi/storify/tanstack";
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { stories } from "../stories";
export const Route = createFileRoute("/ui")({
component: UILayout,
});
function UILayout() {
return (
);
}
```
### 4. Create the index route
```tsx
// src/routes/ui/index.tsx
import { StoryPage } from "@ftzi/storify";
import { createFileRoute } from "@tanstack/react-router";
import { stories } from "../../stories";
export const Route = createFileRoute("/ui/")({
component: UIIndex,
});
function UIIndex() {
return ;
}
```
### 5. Create the catch-all route
```tsx
// src/routes/ui/$.tsx
import { StoryPage } from "@ftzi/storify";
import { createFileRoute } from "@tanstack/react-router";
import { stories } from "../../stories";
export const Route = createFileRoute("/ui/$")({
component: StoryRoute,
});
function StoryRoute() {
const { _splat } = Route.useParams();
const path = _splat ? _splat.split("/").filter(Boolean) : [];
return ;
}
```
## π Writing Stories
```tsx
// app/ui/stories/button.story.tsx
import { story } from "@ftzi/storify";
import { Button } from "@/components/ui/button";
export const Default = story({
render: () => Click me,
});
```
### With Zod Controls
Use Zod schemas to auto-generate interactive controls:
```tsx
import { story } from "@ftzi/storify";
import { z } from "zod";
import { Button } from "@/components/ui/button";
export const Controlled = story({
schema: z.object({
variant: z
.enum(["primary", "secondary"])
.default("primary")
.describe("Button variant"),
disabled: z.boolean().default(false).describe("Disabled state"),
children: z.string().default("Click me").describe("Button text"),
}),
render: (props) => ,
});
```
### Zod β Control Mapping
| Zod Type | Control |
| --------------- | --------------- |
| `z.string()` | Text input |
| `z.number()` | Number input |
| `z.boolean()` | Toggle |
| `z.enum([...])` | Select dropdown |
- `.default(value)` - Sets initial control value
- `.describe("...")` - Adds optional description (shown as tooltip on βΉοΈ icon)
### Story Matrix (Automatic Combinatorial Testing)
This is the killer feature. Instead of manually writing dozens of story variants, let Storify generate ALL combinations automatically:
```tsx
import { storyMatrix } from "@ftzi/storify";
import { z } from "zod";
import { Button } from "@/components/ui/button";
// This single export generates 12 visual tests automatically!
// (3 variants Γ 2 sizes Γ 2 disabled states = 12 combinations)
export const Matrix = storyMatrix({
schema: z.object({
variant: z.enum(["primary", "secondary", "ghost"]),
size: z.enum(["sm", "lg"]),
disabled: z.boolean(),
}),
render: (props) => Click me,
});
```
The matrix view displays all combinations in a grid:
```
βββββββββββββββββββββββ¬ββββββββββββββββββββββ¬ββββββββββββββββββββββ
β primary, sm, false β secondary, sm, falseβ ghost, sm, false β
β [Button] β [Button] β [Button] β
βββββββββββββββββββββββΌββββββββββββββββββββββΌββββββββββββββββββββββ€
β primary, sm, true β secondary, sm, true β ghost, sm, true β
β [Button] β [Button] β [Button] β
βββββββββββββββββββββββΌββββββββββββββββββββββΌββββββββββββββββββββββ€
β primary, lg, false β secondary, lg, falseβ ghost, lg, false β
β [Button] β [Button] β [Button] β
βββββββββββββββββββββββ΄ββββββββββββββββββββββ΄ββββββββββββββββββββββ
... and so on
```
**Why this matters:**
- **Zero boilerplate** - No more writing `PrimarySmall`, `PrimaryLarge`, `SecondarySmall`...
- **Complete coverage** - Never miss a combination again
- **Always in sync** - Add a new variant? The matrix updates automatically
- **Visual regression at scale** - See every state at once, catch issues instantly
## π File Organization
Stories are organized by the keys you provide to `createStories`:
```tsx
export const stories = createStories({
button: () => import("./button.story"), // β "Button"
forms: {
input: () => import("./forms/input.story"), // β "Forms > Input"
select: () => import("./forms/select.story"), // β "Forms > Select"
},
layout: {
card: () => import("./layout/card.story"), // β "Layout > Card"
},
});
```
Named exports become story variants:
```tsx
// button.story.tsx
export const Primary = story({ ... }) // β "Button > Primary"
export const Secondary = story({ ... }) // β "Button > Secondary"
```
## π Mocking API Requests
Storify supports [MSW (Mock Service Worker)](https://mswjs.io/) for mocking API endpoints in your stories. This is useful for testing components that fetch data without hitting real backends.
### Setup MSW
```bash
# Install MSW
npm install msw --save-dev
# Initialize service worker (creates public/mockServiceWorker.js)
npx msw init public
```
### Basic Mocking
Add a `mocks` array to your story to intercept network requests:
```tsx
import { story } from "@ftzi/storify";
import { http, HttpResponse } from "msw";
export const WithMockedData = story({
mocks: [
http.get("/api/user", () => HttpResponse.json({ name: "John Doe" })),
http.get("/api/posts", () => HttpResponse.json([
{ id: 1, title: "First Post" },
{ id: 2, title: "Second Post" },
])),
],
render: () => ,
});
```
When viewing this story, a "Mocks" indicator appears in the header showing that API requests are being intercepted.
### Mock Factories (Dynamic Mocks)
Mocks can be a function that receives control values, allowing dynamic mock responses:
```tsx
export const Configurable = story({
schema: z.object({
userName: z.string().default("Jane"),
shouldError: z.boolean().default(false).describe("Simulate API error"),
}),
mocks: ({ userName, shouldError }) => [
http.get("/api/user", () => {
if (shouldError) {
return new HttpResponse(null, { status: 500 });
}
return HttpResponse.json({ name: userName });
}),
],
render: () => ,
});
```
Now you can toggle `shouldError` in the controls panel to test error states!
### Testing Loading States
```tsx
import { delay } from "msw";
export const Loading = story({
mocks: [
http.get("/api/user", async () => {
await delay("infinite"); // Never resolves
return HttpResponse.json({});
}),
],
render: () => ,
});
```
### Alternative: Prop-Based Mocking
For simpler cases, you can pass mock functions as props (no MSW needed):
```tsx
export const WithMockFetcher = story({
schema: z.object({
fetchUser: z.function().returns(z.promise(z.object({ name: z.string() }))),
}),
render: ({ fetchUser }) => ,
});
```
This approach requires your component to accept the fetcher as a prop.
### Generating Mock Data
For generating realistic mock data from Zod schemas, check out [@anatine/zod-mock](https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock):
```tsx
import { generateMock } from "@anatine/zod-mock";
const userSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
});
export const WithGeneratedData = story({
mocks: [
http.get("/api/user", () => HttpResponse.json(generateMock(userSchema))),
],
render: () => ,
});
```
## π Layout Isolation
If your root layout has providers that conflict with Storify, use `useSelectedLayoutSegment` to skip them for the `/ui` route:
```tsx
// app/layout.tsx
"use client";
import { useSelectedLayoutSegment } from "next/navigation";
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
const segment = useSelectedLayoutSegment();
// Skip providers for Storify
if (segment === "ui") {
return (
{children}
);
}
return (
{children}
);
}
```
Reference: [useSelectedLayoutSegment](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment)
## π‘οΈ Access Control
By default, the generated layout blocks access in production. This condition can be changed to disable Storify based on your needs:
```tsx
// app/ui/layout.tsx
export default function StorifyLayout({ children }: { children: React.ReactNode }) {
// Change this condition to control when Storify is disabled
// if (process.env.NODE_ENV === "production") {
// notFound();
// }
return (
{children}
);
}
```
## π‘ Why Storify?
### The Storybook Problem
With Storybook, you manually write each variant:
```tsx
// Storybook: Write EVERY combination by hand π©
export const Primary = () =>
export const Secondary = () =>
export const Ghost = () =>
export const PrimarySmall = () =>
export const PrimaryLarge = () =>
export const PrimaryDisabled = () =>
export const SecondarySmall = () =>
export const SecondaryLarge = () =>
export const SecondaryDisabled = () =>
// ... 20+ more exports, and you STILL missed some combinations
```
### The Storify Solution
```tsx
// Generates ALL 36 combinations automatically π
export const Matrix = storyMatrix({
schema: z.object({
variant: z.enum(["primary", "secondary", "ghost"]),
size: z.enum(["sm", "md", "lg"]),
disabled: z.boolean(),
loading: z.boolean(),
}),
render: (props) => Click me,
});
```
### Feature Comparison
| Feature | Storybook | Storify |
| ----------------------- | ---------------------------- | ---------------------------- |
| Dependencies | 100+ packages | **Zero** |
| Setup time | ~30 min | ~5 min |
| Separate build | Yes | No |
| Config duplication | Yes (Tailwind, etc.) | No |
| Bundle size | Large | Minimal |
| Hot reload | Separate process | Same as app |
| Framework support | Many (via adapters) | Next.js, TanStack Start |
| **Combinatorial testing** | Manual (write each variant) | **Automatic (storyMatrix)** |
| Variant coverage | Whatever you remember | **100% guaranteed** |
| Maintenance burden | High (keep variants in sync) | **Zero (schema is truth)** |
| AI assistant support | No | **Yes (CLAUDE.md, AGENTS.md)** |
| API Mocking | Addon (msw-storybook-addon) | Built-in (optional MSW) |
## π€ AI-First Design
Storify is the **first component documentation tool with built-in AI assistant support**. When you run `npx @ftzi/storify`, it generates `CLAUDE.md` and `AGENTS.md` files in your stories directory that teach AI assistants how to write stories.
**What this means:**
- **Claude Code** reads `CLAUDE.md`
- **Cursor** reads `AGENTS.md`
- **Other AI tools** can reference these files
The generated instructions include:
- How to write `story()` with examples
- Zod schema patterns for controls
- `storyMatrix()` for combinations
- File naming conventions
- Best practices
**Just say:**
> "Write a story for my Button component"
And your AI assistant already knows exactly how to create a comprehensive story with interactive controls, variants, and even a matrix for all combinations - no explanation needed.
**Upgradable:** Re-running `npx @ftzi/storify` updates the AI instructions section (marked with `` / ``) while preserving any custom instructions you've added outside the markers.
## π οΈ Development
Commands
```bash
# Install dependencies
bun install
# Start the example app
bun dev
# Type check and lint
bun ok
# Run visual regression tests
bun test:e2e
```
## π License
MIT