Compare DocsPage and MDX syntax in Storybook with example

Created

This post compares the DocsPage and MDX syntax to write story of your UI components using Storybook. We will continue with the Card component we talked about in my last article.

There are two ways you can write stories using Storybook: DocsPage and MDX. Both, DocsPage and MDX are made possible by the Storybook addon called Docs.

Compare MDX and DocsPage syntax in storybook
Compare MDX and DocsPage syntax in storybook

DocsPage

DocsPage is a default way of writing component stories with zero configuration where you simply create *.stories.js file to get started. I demonstrated how can use different options to document a Card component and organise text description, code samples, control panels options in the previous article: Create Card component story using Storybook with Nuxt

MDX

MDX gives you lot of freedom to create free-form pages for each component, where we can simultaneously document components and write stories by creating *.stories.mdx file. I've configured my Nuxt project for the Storybook and to use MDX syntax, I have let Storybook know that I wish to use .mdx files for my stories. We can do this by updating our Storybook configuration in nuxt.config.js

// nuxt.config.js
export default {
    // ...
    storybook: {
        // ...
        stories: ["~/components/**/*.stories.mdx"],
    },
};

Unlocking Storybook’s superpower... using MDX this time

Let's recap how the story is made. A story is a function that describes how to render different states of your UI components. There three steps as I see it:

  • First, describe component using export default
  • then define the template
  • and finally, describe story using named exports

The default export metadata controls how Storybook lists your stories.

<!-- card.stories.mdx -->

import { Meta } from '@storybook/addon-docs/blocks';

<meta title="MDX/Card" component="{Card}" />
// card.stories.js
export default {
  title: "JS/Card",
  component: Card
}

Named export is used to define component’s story using Template.

<!-- card.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs/blocks';

<meta title="MDX/Card" component="{Card}" />

<!-- Define template for Primary Story -->
export const PrimaryTemplate = () => ({})

<!-- named export Primary to create respective story -->
<Story name="Primary">
    { PrimaryTemplate.bind({})}
</Story>
// card.stories.js
export default {
  title: "JS/Card",
  component: Card
}

// Define template for Primary Story
const PrimaryTemplate = () => ({})
// named export Primary to create respective story
export const Primary = PrimaryTemplate.bind({});

When using MDX syntax, we will have to make use of <Meta /> and <Story /> components which we can import from Storybook addon-docs/block.

import { Meta, Story } from '@storybook/addon-docs/blocks';

<Meta /> component helps us describe the component and <Story /> component helps us describe the story as show in the code above.👆

We can also wrap the <Story /> into a <Canvas /> component for extra features, i.e get automatically generated source code snippet. In DocsPage, every story is wrapped in a Canvas block by default where we cannot opt out of displaying it. On the other hand, using MDX convention, we can choose to drop this Canvas wrapper component if we want to.

<Canvas>
    <Story name="Primary">
      { PrimaryTemplate.bind({})}
    </Story>
<Canvas/>

Example: Card Component

As an example, I will try to replicate all three stories using MDX: Primary, SVG and Gradient.

Three stories: Primary, SVG and Gradient
Three stories: Primary, SVG and Gradient

The story-outline for above scenario would look something like this.👇

<!-- card.stories.mdx -->

import { Meta, Story } from '@storybook/addon-docs/blocks';

<meta title="MDX/Card" component="{Card}" />

<!-- Primary Story -->

<!-- Define template for Primary Story -->
export const PrimaryTemplate = () => ({})
<!-- named export Primary to create respective story -->
<Story name="Primary">
    { PrimaryTemplate.bind({})}
</Story>

<!-- SVG Story -->

<!-- Define template for SVG Story -->
export const SVGTemplate = () => ({})
<!-- named export SVG to create respective story -->
<Story name="SVG">
    { SVGTemplate.bind({})}
</Story>

<!-- Gradient Story -->

<!-- Define template for Gradient Story -->
export const GradientTemplate = () => ({})
<!-- named export Gradient to create respective story -->
<Story name="Gradient">
    { PrimaryTemplate.bind({})}
</Story>
// card.stories.js
export default {
    title: "JS/Card",
    component: Card,
};

// Primary

// Define template for Primary Story
const PrimaryTemplate = () => ({});
// named export Primary to create respective story
export const Primary = PrimaryTemplate.bind({});

// SVG

// Define template for SVG Story
const SVGTemplate = () => ({});
// named export Primary to create respective story
export const Primary = SVGTemplate.bind({});

// Gradient

// Define template for SVG Story
const GradientTemplate = () => ({});
// named export Primary to create respective story
export const Primary = GradientTemplate.bind({});

Working with props using args and argTypes

args use-case:

Although our Card component has default values for its props, on initial render of the component in Storybook, it shouldn't use default values set for the props, but it should rather have:

  • padding of 3
  • primary-color set to green-400 and
  • border-radius to 3xl

...given that all three props are are available on the component.

In this case, when we export our story, we can set the args values using Primary.arg = {} 👇

<!-- card.stories.mdx -->

<!-- args at component-level -->
<Meta title="MDX/Card"
            component={Card}
            args={{
                padding: 3,
                primaryColor: "green-400",
                borderRadius: "3xl"
            }}/>

<!-- args at story-level -->
<Story name="Primary"
       args={{
                padding: 3,
                primaryColor: "green-400",
                borderRadius: "3xl"
            }}
>
  { PrimaryTemplate.bind({}) }
</Story>
// card.stories.js

// args at component-level
export default {
    title: "JS/Card",
    component: Card,
    args: {
        padding: 3,
        primaryColor: "green-400",
        borderRadius: "3xl",
    },
};

// args at story-level
export const Primary = PrimaryTemplate.bind({});
Primary.args = {
    padding: 3,
    primaryColor: "green-400",
    borderRadius: "3xl",
};

And finally, padding , primaryColor and borderRadius variables should be bound to the story template to take effect and for further dynamic manipulation.

<!-- card.stories.mdx -->

export const PrimaryTemplate = (args, { argTypes }) => ({ props:
Object.keys(argTypes), template: `
<card
    :padding="padding"
    :primary-color="primaryColor"
    :border-radius="borderRadius"
>
    <!-- ... -->
</card>
`, });
// card.stories.js

const PrimaryTemplate = (args) => ({
    // ...
    template: `
  <card
    :padding="padding"
        :primary-color="primaryColor"
    :border-radius="borderRadius"
  >
   <!-- ... -->
  </card>
`,
});

argTypes use-case:

Now, let's say we wish to limit the padding values available for our component. In this case, we will go one step back from the story-level, and onto the component-level and use argTypes option.

<!-- card.stories.mdx -->

<!-- argTypes at component-level -->
<Meta title="MDX/Card" component={Card} argTypes={config.argtypes}/>

<!-- argTypes at story-level -->
<Story name="Primary"
       argTypes = {
        borderRadius: {
            control: {
                type: "select",
                options: ["2xl", "3xl", "lg", "md"]
            },
            defaultValue: "2xl"
        }
      }
>
  { PrimaryTemplate.bind({})}
</Story>
// card.stories.js

export default {
    title: "JS/Card",
    component: Card,
    argTypes: {
        borderRadius: {
            control: {
                type: "select",
                options: ["2xl", "3xl", "lg", "md"],
            },
            defaultValue: "2xl",
        },
    },
};

Now that we have argTypes, we can come down at the story-level and pass it into the story template props. 👇

<!-- card.stories.mdx -->

export const PrimaryTemplate = (args, { argTypes }) => ({ props:
Object.keys(argTypes),
<!-- ... -->
})
// card.stories.js

const PrimaryTemplate = (args, { argTypes }) => ({
    props: Object.keys(argTypes),
});

Extend Storybook using addons

Addons helps us extend Storybook's functionalities. There are different types of addons available for Storybook and you can see the full list of addons here.

Decorator use-case:

Out of the box, component is rendered on the left-hand side of the Canvas and the Docs page. But let's say we want to centre-aligned our component for all stories.

Since we want to apply this behaviour to all stories of a component, it makes sense to apply this change at component-level.👇

// card.stories.js

// decorators at component level
export default {
    title: "JS/Card",
    component: Card,
    decorators: [
        () => ({
            template:
                '<div style="display: flex; align-items: center; justify-content: center;"><story /></div>',
        }),
    ],
};

In case, we needed to apply completely different markup for each stories, then we would apply decorators at story-level like below.

<!-- card.stories.mdx -->

<!-- decorators at component-level -->
<Meta title="MDX/Card"
            component={Card}
            argTypes={config.argtypes}
            decorators=[()=>({template: '<div style="display: flex; align-items: center; justify-content: center;"><story /></div>'})]
/>

<!-- decorators at story-level -->
<Story name="Primary"
       decorators={[() => ({ template: '<div style="padding: 3rem;"><story /></div>' })]}
>
  { PrimaryTemplate.bind({})}
</Story>
// card.stories.js

// decorators at Story-level
// ...
export const Primary = PrimaryTemplate.bind({});

Primary.decorators = [
    () => ({
        template: '<div style="padding: 1rem;"><story /></div>',
    }),
];

Parameter use-case:

We want to add custom text on the doc section of the Storybook for our Card component that should look something like this.👇

Add custom text block using parameter
Add custom text block using parameter

In MDX, we are free to combine text blocks and components, so it may be limiting to use docs.description.component option to add content, because in MDX you can add content as if you are writing any MarkDown file.But this also doesn't mean we can't apply other parameters to MDX based story. See how we can control the layout of a story using parameters.

<!-- card.stories.mdx -->

<!-- parameter at component-level -->
<Meta title="MDX/Card"
      component={Card}
      parameters={{ layout: "centered" }}
/>

<!-- parameter at story-level -->
<Story name="Primary"
       parameters={{ layout: "centered" }}
>
  { PrimaryTemplate.bind({})}
</Story>
// card.stories.js

import readme from "./docs/readme.md";

// parameter at component-level
export default {
    title: "JS/Card",
    // ...
    parameters: {
        docs: {
            description: {
                component: readme,
            },
        },
    },
};

// parameter at story-level

// ...
export const Primary = PrimaryTemplate.bind({});

Primary.parameters = {
    docs: {
        description: {
            component: "You may add readme.md file for Gradient Card here.",
        },
    },
};

Display Code Blocks

One of the fundamental element of documentation is the code samples. With Storybook, we can display code samples in couple of ways.

<!-- card.stories.mdx -->

import { Meta, Story, Props, Source } from "@storybook/addon-docs/blocks";

<!-- Use source tag in mdx to provide the code sample at story level -->
<Source
    language="html"
    code={dedent`
<card
    :padding="3"
    :primary-color="pink-400"
     secondary-color="gray-300"
    :border-width="2"
    :border-radius="xlg"
    class="w-72"
  >
        <!-- ... -->
  </card>
`}
/>;
// card.stories.js

// add source code for individual story using parameters
Primary.parameters = {
    docs: {
        source: {
            code: "`<Card> ... </Card>`",
        },
    },
};
Use <Source /> tag in MDX to generate code sample
Use <Source /> tag in MDX to generate code sample

Conclusion

I have barely scratch the surface of what Storybook has to offer but seeing how Storybook is not opinionated and what's possible with MDX, I'm encouraged to try to create completely custom layouts to document my UI components and examine how far I can go.

Before I finished, let's recap the key concepts of Storybook I covered in this article.

  • args
    • args can be applied at story and component level.
    • args are dynamic data that are provided and updated by Storybook so that you can see your UI component in action and manipulate it dynamically.
    • args can be used to dynamically change props, slots, styles, inputs, etc. This allows Storybook and its addons to live edit components.
  • argTypes
    • argType can also be applied at story and component level.
    • argTypes specify the behaviour of Args and help you constrain the values that args can accept.
  • parameters
    • parameters can be applied at story, component and globally.
    • parameters help you control the behaviour of Storybook features and addons.
  • decorators
    • decorators can be applied at story, component and globally.
    • decorators helps you control how your story is rendered. Most typical use-case of decorators is to wrap stories with additional HTML markup.

I hope you enjoyed reading this article. For more demos and articles on Nuxt and Vue, you can follow me on Twitter @KrutiePatel.

· · ·