We all love Storybook. It’s the “No one ever got fired for buying IBM” of design system documentation1.

This post is going to assume a reasonable working knowledge of Storybook; storybook itself has excellent documentation on how to configure controls and there’s no sense in reproducing that here.

One of its great features is the ability to generate all the knobs and levers necessary to control your components, so that with minimal effort you can create a fully interactive demo of your component, complete with docstrings extracted from source.

However, there are some limitations to this:

  1. It’s only available for React and Vue (as far as I’m aware)
  2. Because it magically ‘just works™’, when it doesn’t, you’re left in the lurch with having to manually override controls in the story meta.

So, if you’re sitting outside of those two conditions, you’re stuffed.

Or are you?

An excellent posting was shared on the Storybook discord server recently:

Documenting Web Components With Storybook

In it, Jives describes their approach to generating and injecting controls for Web Components, via the intermediate form of a Custom Elements Manifest.

I’d recommend reading the post, but broadly it involves a transform function to turn a Custom Element Manifest entry into a Storybook control definition, and then injecting it manually into the meta.

This struck a chord with me, as I’d been taking a similar approach for my own Design System work, even though I was using React.

Why not use the built-in docgen?

To take a step back, I should first explain why I was using an external source for my docgen.

Firstly, I wasn’t satisfied with the information coming out of either react-docgenreact-docgen or react-docgen-typescriptreact-docgen-typescript. This is not to besmirch them as not being any good - I think they’re both wildly impressive bits of software, but they weren’t producing all the information I needed.

Some of this was related to React (I wanted to know what underlying HTML element the component was forwarding to), and some completely outside react (I wanted to document which CSS @property values were defined and consumed by this component.

Secondly, I had other potential consumers of this manifest of information - I needed to have a persistent manifest stored within the repo. Given this had all the information necessary for generating the controls, why generate it again within Storybook?

I now have my manifest all its juicy information, and I have my stories. Sure, I could take the approach Jives did, but coming from a place where the controls were automatically injected, I didn’t want to lose that convenience.

How to inject?

So began my journey of discovery as to how Storybook docgen magic works. I had been using react-docgen-typescript. Spoilers - it ain’t pretty.

To use React + Vite as an example:

Step one is to inject the storybook:react-docgen-plugin. This adds a vite transformer that runs react-docgen over each source file, then weaves in a JSON serialised representation of the generated docs:

if (actualName && definedInFile == id) {
  const docNode = JSON.stringify(docgenInfo);
  s.append(`;${actualName}.__docgenInfo=${docNode}`);
}

At runtime, this injected data is pulled through by:

  1. read in getDocgenSection
  2. called by extractComponentProps
  3. called by extractProps
  4. called by extractArgTypes
  5. assigned to preview.docs.extractArgTypes
  6. read by enhanceArgTypes
  7. which was configured in the preview’s argTypesEnhancers

phew!

My first attempt at doing this was to write my own vite plugin that simulated the behaviour of the Storybook plugin, but read from my manifest file rather than generating on the fly.

This was a bit of a pain, as aside from the difficulty of finding the right object to inject the info to (e.g., injecting after wrapping in memo), the format for __docgenInfo in React is react-docgen, so I had to massage my manifest docs into this format only so Storybook could turn them back into Storybook Control format.

The More Undifficult Way

When I came back to look at this again, I realised that rather than injecting at the very top of the call tree, I could bypass all of Storybook’s shenanigans if only I investigated this argTypesEnhancers nonsense. It’s a feature that seems to lack any external documentation that I can find.

However, the type definition:

export type ArgTypesEnhancer<TRenderer extends Renderer = Renderer, TArgs = Args> = (
  context: StoryContextForEnhancers<TRenderer, TArgs>
) => StrictArgTypes<TArgs>

and usage:

contextForEnhancers.argTypes = argTypesEnhancers.reduce(
  (accumulatedArgTypes, enhancer) =>
    enhancer({ ...contextForEnhancers, argTypes: accumulatedArgTypes }),
  contextForEnhancers.argTypes
);

are pretty easy to grok, and so writing a custom ArgTypesEnhancer becomes incredibly trivial:

import { combineParameters } from "@storybook/preview-api";
import type { ReactRenderer } from "@storybook/react";
import type { ArgTypesEnhancer } from "@storybook/types";

import manifest from './manifest';

export const argTypesEnhancer: ArgTypesEnhancer<ReactRenderer> = (context) => {
    const docArgTypes = conformArgs(manifest[context.component.displayName].properties);
    return combineParameters(docArgTypes, context.argTypes);
}

The only difficult part being the implementation of conformArgs which needs to convert from however you’ve stored your documentation to Storybook Controls. Once that’s done, just export this from your preview.ts and you’ll be all done!

Bonus: args

As a bonus, a very similar mechanism can be used to inject default args, using the appropriately named argsEnhancer.

So what?

Why write this blog post? Well, a. because someone asked me too, b. I think it’s a pretty nifty feature that I wanted to share and c. as a starting point for improving documentation in this area.

I think it’s a really powerful API that Storybook would benefit from documenting more. It’s not some weird internal magic (like __docgenInfo is), so I would hope there wouldn’t be a problem with publicising it further.

I think there are also further extensions to this style of API that could be added - Jives’ post includes the binding of action handlers, which could equally be automatically injected from generated documentation.


  1. This is perhaps a little unfair on Storybook - it’s a great piece of software in its own right. It just absolutely dominates the landscape