Storybook Docs with mdx support and addon knobs

Michele
GumGum Tech Blog
Published in
6 min readJul 14, 2020

We have used Storybook to document our React component library for a while, and although we love it, we’ve had to use various addon packages and do some hacky things to get components to show how we wanted. Storybook has undergone some big versioning changes recently, so I sat down to upgrade everything from 5.0 to the latest version and see what changed.

After upgrading all of the storybook npm packages, I see that the config files have completely changed. I can remove .storybook/addons.js and .storybook/config.js

Instead, I need:

// .storybook/main.jsmodule.exports = {
stories: ['../_stories/**/*.stories.@(js|mdx)'], // adding mdx support here
addons: [
'@storybook/addon-knobs/register',
{
name: '@storybook/addon-docs', // new addon for docs
options: {
configureJSX: true,
babelOptions: {},
sourceLoaderOptions: null
}
},
]
};

and

// .storybook/preview.jsimport React from 'react';
import { configure, addDecorator, addParameters } from '@storybook/react';
import { DocsPage, DocsContainer } from '@storybook/addon-docs/blocks';
import { withInfo } from '@storybook/addon-info';
import { create } from '@storybook/theming';
import './index.scss';addParameters({
// This whole section needs to be added for docs support
docs: {
container: DocsContainer,
page: DocsPage
}
});
function loadStories() {
require('../_stories/');
}
const storyWrapper = story => <div style={{ margin: 35 }}>{story()}</div>;addDecorator(
withInfo({
inline: true,
header: false,
source: true,
maxPropsIntoLine: 1
})
);
addDecorator(storyWrapper);configure(loadStories, module);

Okay, not bad, everything seems to be working after that.

Docs addon

Next, I try to make sense of the new docs addon, which seems like it handles documentation now out of the box, AND allows for markdown, which we currently implement through a community addon called storybook-readme. (Technically, this new docs addon supports mdx, which lets you combine markdown and jsx.) Ideally, I would think I can remove storybook-readme and use the official addon now!

We are using the readme addon because we have some more complicated components, and we wanted to write their documentation in markdown for ease.

I learn that with the latest Storybook version, in the most simple version, the docs are auto-generated for you, which is not exactly what we want.

Since we want to use mdx to write our documentation instead, we need to use the approach CSF stories with arbitrary mdx.

Note:

  • Storybook also provides a way for you to tell it just to use mdx, but since we also use other addons (most notably, we really like using knobs), we don’t want to do that approach either.

Here’s what one of our most basic stories ended up as…

This will show up on the main page where we can see the component and interact with it using knobs:

// Badge.stories.jsimport React from 'react';
import { withKnobs, object, text, boolean, select } from '@storybook/addon-knobs/react';
import mdx from './Badge.mdx';
import Badge from '../../../components/atoms/Badge';
const options = {
'No Value': '',
inverse: 'inverse',
success: 'success',
'success-inverse': 'success-inverse',
info: 'info',
'info-inverse': 'info-inverse',
warning: 'warning',
'warning-inverse': 'warning-inverse',
danger: 'danger',
'danger-inverse': 'danger-inverse'
};
export default {
component: Badge,
title: 'Atoms/Badge',
decorators: [withKnobs],
parameters: {
docs: { page: mdx }, // Need to add this param to show the mdx docs
}
};
export const Default = () => {
return (
<Badge
text={text('Text', '2')}
context={select('Context', options, '')}
empty={boolean('Empty', false)}
className={text('ClassName', '')}
style={object('Style', {})}
/>
);
};

Notes:

  • I kept the addon info to display the source of each story. If embedding the story directly into a mdx file, the source won’t show when wrapping the embedded <Story> with <Preview>. To get this source code to show up, we need to add source: true in the example .storybook/preview.js file at the top of this article. I find showing the source to be really helpful since you can copy and paste it to use in your code.
  • The “Default” in export const Default is what you have your component titled as in the side nav. You can have multiple versions of components show up here if you want. We normally just use knobs to show different variations, but we do use this in our Pagination component to show different configs.

This will show up on the Documentation tab:

// Badge.mdximport { Story } from '@storybook/addon-docs/blocks';
import Badge from '../../../components/atoms/Badge';
# BadgeThe `<Badge>` component is a numerical indicator of associated items. For a simple colored circle without a number inside, pass in the "empty" prop. If empty, it will not display any text within the badge.## Component example<Story id="atoms-badge--default" /> // see note below on what id to put here

Including the <Story> here lets someone see an example of how the component looks right on the documentation, as well as showing the props accepted and a copy and paste version of the actual component code that you can use.

Note:

My component needs state!

We have a few more complex components that require us to keep track of state for them to work properly. The best approach I found for doing this can be seen on the Pagination component.

import React from 'react';
import { withKnobs, number, boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import mdx from './Pagination.mdx';
import Pagination from '../../../components/molecules/Pagination';
const sizeOptions = {
xl: 'xl',
lg: 'lg',
sm: 'sm',
xs: 'xs',
'No Value': ''
};
export default {
component: Pagination,
title: 'Molecules/Pagination',
decorators: [withKnobs],
parameters: {
component: Pagination,
docs: { page: mdx }
}
};
class PaginationStory extends React.Component {
state = {
activePage: 7,
lastPage: 70
};
handlePageChange = newPage => {
const activePage = newPage;
this.setState({ activePage });
action('Page change')(activePage);
};
render() {
const { activePage, lastPage } = this.state;
return (
<Pagination
activePage={number('Current Page', activePage)}
lastPage={number('Last Page', lastPage)}
boundaries={boolean('Show Boundaries', false)}
justify={boolean('Justify', false)}
size={select('Size', sizeOptions, '')}
onChange={this.handlePageChange}
/>
);
}
}
PaginationStory.displayName = 'Pagination';export const Default = () => { return <PaginationStory />; };Default.story = {
parameters: {
info: { source: false }
}
};

You can see here that I define a class component, then export that component. The one thing to note here is I overrode the story parameters to not show the source. This is because since you’re exporting <PaginationStory /> , you would just see that, which isn’t helpful. I would want to see the source for <Pagination> instead, which isn’t possible, so I choose to hide it instead. In the component’s mdx page, I give examples of the component.

Prop Tables

Prop tables on the documentation pages are much improved. These are populated directly from your component propTypes, but there’s a few extra things to note:

Description:

  • Use a comment above each propType to add a description that will show up in the table /** My description here */
  • Using PropTypes.oneOfType(['optionA', 'optionB']) fills out the description table with the accepted options

Required:

  • Adding .isRequired to your propType adds a * to it in the table

Default:

  • To indicate a default value, use defaultProps in your components.

Props for Subcomponents:

  • Sometimes components require sub components to work. You can now indicate these and show the propTypes needed for each of the other components. See our Accordion component. This can be achieved by modifying your story parameters to include “subcomponents”.
// Accordion.stories.js
parameters: {
component: Accordion,
subcomponents: { AccordionItem, AccordionItemContent },
docs: { page: mdx }
}

Documentation only stories

Finally, we also have some documentation only components that Storybook now supports in an easier way through MDX:

// Name any files Name.stories.mdx; this one is Installation.stories.mdximport { Meta } from '@storybook/addon-docs/blocks';<Meta title="Getting Started/Installation" /> // This is the path you want it to show at## Installation
Docs here

Since we started using Storybook over 3 years ago, it’s been great to see it constantly evolving and improving. Their new Docs addon makes it more seamless to integrate the documentation part of your UI components, even if it needs a bit of customization. Hopefully this article helps you understand how to integrate both Docs with other addon packages in a more straightforward way. You can see the entire diff of this change on our GitHub page.

We’re always looking for new talent! View jobs.

Follow us: Facebook | Twitter | | Linkedin | Instagram

--

--