@tchwrks/storybook-text-search v0.0.7
@tchwrks/storybook-text-search
is a Storybook addon that enables full-text search across MDX content:
- š Builds an MDX/documentation index at startup (Stories / autodocs parsing on the roadmap)
- š” Integrates a "search bar" toolbar trigger and modern search modal into the Storybook manager UI
- ā»ļø Automatically rebuilds index on hot reload
- š¹ 100% keyboard accessible
- āļø Works with both Vite and Webpack Storybook builders
- āļø Written in (mostly) TypeScript using the official Addon Kit
- š Sub 200KB index for >20 MDX files
- šļø Fast local search. No server needed. No 3-5 business days for results
ā ļø Note: This addon has not performed the Storybook 9.0 migrations yet. A new major version supporting Storybook 9.0 will be released soon. This addon also does not currently support the React Native framework. Limited functionality for non-react frameworks
Table of Contents
Getting Started
Installation
First, install the addon:
yarn add -D @tchwrks/storybook-text-search
# or
npm install --save-dev @tchwrks/storybook-text-search
# Optionally, to install and register the addon in .storybook/main#addons
yarn dlx storybook add @tchwrks/storybook-text-search
# or
npx storybook add @tchwrks/storybook-text-search
Configuration
"Trust the process"
This addon ships with an initialization script to get you going:
npx storybook-text-search-init
This script will:
- Generate
.storybook-text-search
which houses the config and artifact(s) - Read your
.storybook/main.{ts,js}
and generate a default.storybook-text-search/config.js
with sensible defaults - Register the addon and its artifacts in
.storybook/main.{ts,js}
- Generate an initial index.
For most setups, this is all you need to get going. Just start Storybook like usual and enjoy!
"I'll do it myself"
Create the tool / addon directory,
.storybook-text-search/
at your project's root (or at the same level as your.storybook/
Create a
.storybook-text-search/config.mjs
:// Custom extractors. See `advanced configuration` for more import { tableExtractor, colorPaletteExtractor, apiEndpointExtractor, typeScaleExtractor } from "./custom-extractors/index.js"; const config = { // inputPaths ā stories from .storybook/main.{ts,js}. Glob ext should be written like example // ...for single dir, still place inside an array inputPaths: ["../src/stories/**/*.mdx", "../src/**/*.stories.{js,jsx,ts,tsx}"], // Recursion level/component depth for jsx parsing maxJsxDepth: 5, // Props to parse for fallback jsx parse (opt for jsxTextMap entry when possible) jsxPropAllowList: ['alt', 'title', 'aria-label', 'placeholder', 'label', 'value', 'children'], }; export default config;
Customize this as needed to index the right files and extract meaningful metadata for your docs. See advanced configuration for more
Create
.storybook-text-search/artifacts
directoryRegardless of whether you used
storybook add
for install, make sure you register the addon + artifacts dir from the previous step in your.storybook/main.{ts,js}
:// .storybook/main.ts (React + Vite) import type { StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { stories: ["../src/stories/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], addons: [ "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", // Register addon. Order determines toolbar placement (last = right) // ...running `storybook add` automatically registers addon "@tchwrks/storybook-text-search" ], // Register artifacts dir. Order does not matter staticDirs: ['../.storybook-text-search/artifacts', '../src/assets'], framework: { name: "@storybook/react-vite", options: {}, }, docs: { autodocs: "tag", }, core: { builder: '@storybook/builder-vite', } }; export default config;
Finally, run
npx storybook-text-search-build
or start your Storybook (index will build during startup)
Usage
Usage of this addon is pretty straightforward once you set it up. Just add *.mdx
docs into the paths you defined in your .storybook-text-search/config.mjs
and they'll be automatically indexed on Storybook startup and hot reload.
When Storybook is running, you can open the search modal by clicking the trigger in the toolbar labeled "Find MDX Text" or by using the keyboard shortcut Shift + Cmd + K
. Once open, you can use your mouse/trackpad or the following keyboard shortcuts to navigate:
Shortcut | Action |
---|---|
Shift + Cmd + K | Open search modal |
ā / ā or Tab | Navigate between results |
Enter | Jump to selected result |
Esc | Close the search modal |
You can adjust the positioning of the search modal trigger / search bar by adjusting where you place @tchwrks/storybook-text-search
in your .storybook/main
addon array. Beginning of array = leftmost toolbar spot, end of array = rightmost toolbar spot.
While the addon can pick up on raw markdown and parse basic text rendered in JSX as children or props by leveraging just the maxJsxDepth
and jsxPropsAllowList
fields in the config.mjs
, you may find that it does not fully parse or normalize the text. In which case, please consider leveraging the jsxTextMap
field in the config.mjs
. It allows you to map out extraction behavior for JSX on a per-component basis. See Advanced Configuration for further documentation on this.
If for whatever reason you would like to re-run the initialization script or manually trigger an index rebuild, you can run either of the following scripts:
npx storybook-search-init # addon initialization
npx storybook-search-build-index # build index
A hosted Storybook with expanded documentation and a live demo will be released soon
Note on Framework Compatibility
This addon works with all Storybook frameworks that support MDX, including React, Vue, and Angular.
However, advanced text extraction from JSX components (like parsing strings from nested props or child elements) is currently optimized for React-style JSX. If you're using Vue or Angular, the addon will still index standard Markdown and inline HTML correctly ā just avoid framework-specific bindings or dynamic components in .mdx
files.
Tip: For the best results in Vue or Angular or other non-react frameworks, write MDX with plain HTML/Markdown content. Full JSX-style extraction for other frameworks may be added in the future. If you are interested in accelerating that timeline, reach out.
Advanced Configuration
As mentioned in the Usage section, this addon does a pretty decent job of parsing text for most basic *.mdx
files. However, MDX files can contain JSX, and that JSX might render text at any arbitrary depth and through non-string props. That's where the jsxTextMap
field in the .storybook-text-search/config.mjs
comes into play.
This is where things get lengthy. You've been warned . . .
Full config.mjs
Options
Field | Type | Description |
---|---|---|
inputPaths | string[] | MDX and story globs. Use .{.foo, .bar} for glob extensions |
maxJsxDepth | number | Recursion depth for parsing JSX |
jsxPropsAllowList | string[] | JSX props to extract using default extraction logic |
jsxTextMap | Record<string, ExtractionRule> | Per-component mapping |
Extraction Rule Type
export type ExtractionRule =
| string[] // prop name shorthand
| {
props?: string[]; // prop name shorthand
children?: boolean; // has children
nestedTextSelectors?: string[]; // Nested component / element names
}
| ((node: any) => string[]); // custom extraction function
Utilizing config.jsxTextMap
A configuration file utilizing custom extraction logic might look like the following
// .storybook-text-search/config.mjs
import {
tableExtractor,
} from './custom-extractors/index.js';
const config = {
// inputPaths ā stories from .storybook/main.{ts,js}. Glob ext should be written like example
// ...for single dir, still place inside an array
inputPaths: ['../src/stories/**/*.mdx', '../src/**/*.stories.{js,jsx,ts,tsx}'],
// Recursion level/component depth for jsx parsing
maxJsxDepth: 5,
// Props to parse for fallback jsx parse (opt for jsxTextMap entry when possible)
jsxPropAllowList: ['alt', 'title', 'aria-label', 'placeholder', 'label', 'value', 'children'],
// Custom / more explicit parsing
jsxTextMap: {
// Shorthand (similar to jsxPropAllowList, just scoped)
Badge: ['callout'],
// Example: <TextBlock>Text here</TextBlock>
TextBlock: ['children'],
// Useful for if you have a complex nesting of plain string props
// Example: <Callout message="..." >Text</Callout>
Callout: {
props: ['message'],
children: true,
nestedTextSelectors: ['BodyText'],
},
// For when sh*t gets hairy
Table: {
extractor: tableExtractor // (node) => string[]
},
};
export default config;
When defining ExtractionRule
functions (Table from the code above), it is recommended you write them as plain *.js
files. ESM vs CJS shouldn't make a difference--but you likely won't need extra deps anyway.
Given an a MDX file that contains a Table
component that looks like the following:
import React from "react";
type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
};
type Props<T> = {
columns: Column<T>[];
data: T[];
className?: string;
};
export function Table<T extends Record<string, any>>({ columns, data, className }: Props<T>) {
return (
<table className={className} style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
{columns.map((col, i) => (
<th
key={i}
style={{ borderBottom: "1px solid #ddd", textAlign: "left", padding: "8px" }}
>
{col.header}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<tr key={rowIndex}>
{columns.map((col, colIndex) => (
<td
key={colIndex}
style={{ borderBottom: "1px solid #f0f0f0", padding: "8px" }}
>
{col.render ? col.render(row[col.key], row) : row[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
... a custom ExtractionRule
function for that component might look like this:
// .storybook-text-search/custom-extractors (or wherever you want to put it--knock yourself out)
export function tableExtractor(node) {
const strings = [];
const columnsAttr = node.attributes?.find(attr => attr.name === 'columns');
const dataAttr = node.attributes?.find(attr => attr.name === 'data');
const extractFromArrayExpression = (valueNode) => {
if (valueNode?.type !== 'mdxJsxAttributeValueExpression' || typeof valueNode.value !== 'string') return [];
try {
const evalFunc = new Function(`return (${valueNode.value})`);
const parsed = evalFunc();
if (Array.isArray(parsed)) {
return parsed;
}
} catch (err) {
console.warn('ā ļø Failed to parse Table attribute value:', err);
}
return [];
};
const columns = extractFromArrayExpression(columnsAttr?.value);
const data = extractFromArrayExpression(dataAttr?.value);
for (const col of columns) {
if (typeof col?.header === 'string') {
strings.push(col.header);
}
}
for (const row of data) {
for (const key in row) {
if (typeof row[key] === 'string') {
strings.push(row[key]);
}
}
}
return strings;
}
Want to get back to where you were before? I got you
About
Built by Techworks Studio
Techworks Studio is a hybrid consultancy and R&D studio crafting tools and solutions that empower bold ideas, redefine workflows, and deliver meaningful user experiences
Other goodies from Techworks
- tokenXtractor - Free Figma plugin for extracting local variables (tokens) into a more code-ready format. Control which collections get exported and how
- Figma Plugin React Template (2024) - TS template for creating Figma plugins with a React for the UI
Connect
This addon is built and maintained by Noah Davis of Techworks Studio
Have feedback, ideas, or a collaboration in mind? Reach out anytime
License
Roadmap
- Storybook 9.0 migration + major bump
- Demo-embedded docs
- React: Parse stories + component definitions (for autodocs)
- Advanced searches (multi-keyword, boolean operations, etc)
- Expanded config
Ready to stop playing Where's Wally with your Storybook docs?
yarn add -D @tchwrks/storybook-text-search
# or
npm i -D @tchwrks/storybook-text-search
npx storybook-search-init
Yup. Up and running in just 2 commands . . .