An open API service indexing awesome lists of open source software.

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

Awesome Lists containing this project

README

          


Storify


Zero-config component stories for React frameworks

[![npm](https://img.shields.io/npm/v/@ftzi/storify)](https://www.npmjs.com/package/@ftzi/storify)
[![npm](https://img.shields.io/npm/dt/@ftzi/storify)](https://www.npmjs.com/package/@ftzi/storify)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](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