DEV Community

Cover image for Storybook for everyone: CSF vs. MDX
Laura Carballo
Laura Carballo

Posted on

Storybook for everyone: CSF vs. MDX

Today, I'm going to talk about Storybook v6. It is such a great tool to design, build, document and test isolated components and organize a perfect component library.

The Component Story Format (CSF) is the recommended way of writing stories but more recently, Storybook introduced the option of writing stories using MDX Format so we can easily document our components with a format that we are all very familiar with.

In this post I'll be covering both ways of writing stories with the aim of showing some benefits of both tools and let you choose what works best for your project.

I'll be using a simple Avatar component to serve as example and we'll be creating our stories based on it. I usually build my component libraries using React and Style Components so that's what we'll be using today too.

Our Avatar will look like this:

import styled from "styled-components";
import PropTypes from "prop-types";

export default function Avatar({ src, size, className, alt }) {
  return <Image size={size} className={className} src={src} alt={alt} />;
}

const Image = styled.img`
  border-radius: 100%;
  height: ${(props) => sizes[props.size]};
  width: ${(props) => sizes[props.size]};
`;

const sizes = {
  small: "30px",
  medium: "60px",
  large: "160px",
};

Avatar.propTypes = {
  /**
  The display src of the avatar
  */
  src: PropTypes.string,
  /**
  The display size of the avatar
  */
  size: PropTypes.oneOf(Object.keys(sizes)),
  /**
  Preserve className for styling
  */
  className: PropTypes.string,
  /**
  Include alt tag for accessibility
  */
  alt: PropTypes.string,
};

Avatar.defaultProps = {
  size: "medium",
  src: "/defaultAvatar.svg",
  alt: "avatar",
};
Enter fullscreen mode Exit fullscreen mode

For those new to Storybook, a story is composed of a Canvas, which shows our rendered component and a Docs Block, which are the building blocks of Storybook documentation pages. The PropTypes function will be used in our Docs Block later to show our ArgsTable with all the args (props) included in our component.

Component Story Format (CSF)

Let's begin with Storybook's recommended syntax, CSF.

In CSF, stories are defined as ES6 modules. Consequently, each story is composed of a single default export and one or multiple named exports.

The default exports serves as a default structure for all the stories that we'll write for a component, in our case, the Avatar.

// Avatar.stories.js/ts

import React from "react";
import Avatar from "../components/Avatar";

export default {
  title: "Components/Avatar",
  component: Avatar,
};
Enter fullscreen mode Exit fullscreen mode

The named export is a function that describes how to render a component, so it would simply look like this:

export const Default = () => <Avatar src="/defaultAvatar.svg" size="medium" />;
Enter fullscreen mode Exit fullscreen mode

But we're usually building our component library for documentation purposes so we want to be able to interact with our story and check every use case. For this reason, it is quite handy to include args so we can benefit from the Controls addon and Actions addon.

In the Storybook documentation they use the .bind() method to create a reusable template to pass the components args to each of the component's stories. This comes in handy when we're going to have multiple stories from a single component since it reduces code repetition.

const Template = (args) => <Avatar {...args} />;

export const Default = Template.bind({});
Default.args = {
  src: "/defaultAvatar.svg",
  size: "medium",
  alt: "avatar",
};

export const Profile = Template.bind({});
Profile.args = {
  src: "/lauraAvatar.svg",
  size: "small",
  alt: "user profile",
};
Enter fullscreen mode Exit fullscreen mode

But if you're working closely with designers or other colleagues that are not too comfortable or familiar with the .bind() method, passing args within each story is also good. We end up having a bit more repetition but the code is more readable and you can save yourself the JavaScript lesson.

export const Default = (args) => <Avatar {...args} />;
Default.args = {
  src: "/defaultAvatar.svg",
  size: "medium",
  alt: "avatar",
};
Enter fullscreen mode Exit fullscreen mode

Alright, so now that we have completed our Avatar story we should probably include some documentation for it. That's when using CSF can get a bit tricky.

To add a description in our main Story, we need to insert it inside the parameters object usign parameters.docs.description.component in our export default or parameters.docs.description.story for our named exports.

export default {
  title: "Components/Avatar",
  component: Avatar,
  parameters: {
    component: Avatar,
    componentSubtitle:
      "An Avatar is a visual representation of a user or entity.",
    docs: {
      description: {
        component: "Some description",
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode
const Template = (args) => <Avatar {...args} />;
export const Default = Template.bind({});
Default.args = {
  src: "/defaultAvatar.svg",
  size: "medium",
  alt: "avatar",
};
export const Profile = Template.bind({});
Profile.args = {
  src: "/lauraAvatar.svg",
  size: "small",
  alt: "user profile",
};
Profile.parameters = {
  docs: {
    description: {
      story: `This is a story`,
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

As you can see, this is a bit of a tedious way of writing documentation.

MDX

Writing stories with MDX fixes the previous issue. It allows anyone familiar with the simple Markdown .md format to write documentation. The main benefit is that now non-technical team members can be a part of documenting component libraries.

Designers can now share their design tokens and write documentation to make developers understand the reason behind design patterns. Here is a very cool example from the Storybook MDX announcement article. Philip Siekmann created an amazing addon that generates design token documentation from your stylesheets and asset files and the demo file is documented using MDX.

Imagine that in our Avatar story we want to include documentation that ensure our component's best practices. MDX makes this super easy.

import { Meta, Story, Canvas, ArgsTable } from "@storybook/addon-docs/blocks";
import Avatar from "../components/Avatar";

<Meta
  title="Components/Avatar"
  component={Avatar}
  argTypes={{
    src: {
      name: "source",
      control: { type: "text" },
    },
    size: {
      name: "size",
      defaultValue: "medium",
      control: {
        type: "select",
        options: ["small", "medium", "large"],
      },
    },
  }}
/>

# Avatar

An `Avatar` is a visual representation of a user or entity.

The `small` size should only be used inside a `navbar`.

We should always make sure that we provide an alternative text for screen readers. Therefore, always ensure that the `alt` tag is being used in the component.

<Canvas>
  <Story
    name="Default"
    args={{
      src: "/defaultAvatar.svg",
      size: "medium",
      alt: "default"
    }}>
    {(args) => <Avatar {...args} />}
  </Story>
</Canvas>;

<ArgsTable of={Avatar} />
Enter fullscreen mode Exit fullscreen mode

The ArgsTable is going to render a table in the Docs Blocks with all of the available args. Notice that when using MDX even if we have the Controls addon we can't interact with the component's arguments dynamically in the Docs Blocks but we can customise this table using argsType inside the <Meta /> component.

As you can see, the MDX structure is very similar to CSF since in the back, Storybook will compiled the MDX files into CSF.

Source Dynamic code snippet

If you're using decorators in your component, you might have encountered something like this inside the source code snippet:

// CSF
<div
  style={{
    margin: "2em",
  }}
>
  <No Display Name />
</div>
Enter fullscreen mode Exit fullscreen mode
// MDX
<MDXCreateElement
  mdxType="div"
  originalType="div"
  style={{
    margin: "2em",
  }}
>
  <No Display Name />
</MDXCreateElement>
Enter fullscreen mode Exit fullscreen mode

Decorators provide extra "external" information about a component like extra markup for styling, providing side-loaded data or including required context as ThemeProvider. We can use them globally but also render decorators individually inside each component. When used locally inside our component, these decorators are causing some issues inside the source code snippet.

There are plans of fixing this in the next version but currently, you can use a workaround changing the source code to code using parameters.docs.source.type.

export default {
  title: "Components/Avatar",
  component: Avatar,
  parameters: {
    docs: {
      source: {
        type: "code",
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

I have created a PR for Storybook in an attempt to fix this, so it would be great to get any feedback from you all!

Conclusion

Both CSF and MDX provide a great way of building component libraries. Choosing one over the other mainly depends on your team structure or the usage your planning on having from your component library.

I recently ran a twitter poll on prefered method when writing stories and nearly 70% of the people (80 votes aprox.) voted on using CSF, which is understandable as it is still the standard and recommended way. But, MDX is still a very convenient way of writing stories in those cases where CSF seems a bit of a barrier for non technical users or our component needs precise and well structured documentation.

In my opinion you should not pick between CSF or MDX, they are both a great way of writing stories and both work best when complementing each other.

Top comments (0)