How to build AWSM docs with Storybook and Astro
Tags: frontend, designsystem, tutorial, javascript
Published: 2024-02-26 11:42:00

## Introduction

In the dynamic realm of UI development, a [well-documented design system](https://dev.to/genedesign/design-systems-beyond-basics-unveiling-nuances-and-oversights-280m) is the key to success. Welcome to the AWSM Docs - a slingshot approach to get your Design System documentation website up and running in no time. In this short article, we'll delve into leveraging Storybook and Astro capabilities in order to save time on documentation effort and maintenance.

## Idea

[Storybook](https://storybook.js.org/) and [Astro](https://astro.build/) is the dynamic duo behind the AWSM Docs powerful synergy.

Storybook acts as an interactive playground for UI components, while Astro builds and enhances a static website with dynamic capabilities. Together, they empower UI library development. And the best part is - you get documentation website at (almost) no additional effort.

Of course Storybook *alone* can serve as a documentation website, especially at the early stages of Design System development, however it is limited in terms of customization, and rather opinionated in the sense of information architecture. Let the tool remain the tool for its purpose and embrace the beauty of the *dedicated* documentation website, which can further become a single gateway for information about your Design System.

Bid farewell 👋 to the challenge of maintaining consistency between stories and doc snippets. AWSM Docs approach eliminates redundancy and ensures your documentation mirrors the current state of your UI library or/and Design System.

## Setup

Project structure is quite standard and straightforward. It's a pnpm-based monorepo, additionally enhanced with [nx](https://nx.dev/) and a couple of formal configurations.

Feel free to explore the [repo](https://github.com/XOP/awsm-docs) and read along.

![awsm-docs project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2apzbe2b7x5jima37h1i.png)

`packages/awsm-ui` represents the UI library that needs to be developed and documented. Development premises are provided by Storybook, located in `apps/storybook`. Separate Storybook app is the best solution for scalability, as it can consume stories and MDX files from different packages. This is particularly useful for the modularized libraries, but even without that it offers a more clean code structure thanks to separation of concerns.

Next to Storybook sits the `apps/docs` app, (auto-)[documentation website](https://awsm-docs.vercel.app/) built with Astro, the primary subject of this article. To mimic a real-life Design System website it has a formal structure and some demo pages, traditionally in the form of MD files. The only thing it does not have is the coded UI examples. Interesting. We'll delve into details shortly.

![awsm-docs website - Switch component page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u6furtooeqwy2tuv6b3e.png)

And last but not least, apps and packages depend on the custom [Design Tokens](https://www.design-tokens.dev/), exported by `packages/awsm-tokens`. Note that there are two exported themes, suitable for both light (⚡️ *electric*) and dark (🦄 *ultraviolet*) color mode preferences.

## UI Library

At this point it's worth mentioning that there are a lot of things in this project "for demo purposes". Sure. I am not *totally* opposed to the idea of using three different headless libraries for building a perfect UI, there can be more - yet sanity is the limit.

For the same reasons I selected React as the framework for the task. Love it or hate it, [numbers](https://npmtrends.com/angular-vs-react-vs-svelte-vs-vue) are talking, not to mention the foundational support of its full-stack endorsers - NextJS and Remix.

In `awsm-ui` you may find the 3 
amigos
 components, each based on a different headless UI library (and I'm not going to repeat that further - *for demo purposes*):

* (mandatory) `Button` - based on [React-Aria](https://react-spectrum.adobe.com/react-aria/) - represents the *atomic* component, entity with the simple structure
    
* `Switch` - powered by [Radix UI](https://www.radix-ui.com/primitives) - atomic component with internal state and slightly more complex structure
    
* `Disclosure` - featuring [Headless UI](https://headlessui.com/) - more of a *molecule* kind... perhaps because it's bigger
    

![react-aria, headless UI and radix + vanilla-extract](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6de0yn5ttks1oxi28un.png)

All components are styled with [vanilla-extract](https://vanilla-extract.style/) styling engine, effectively utilizing Design Tokens in JS format. Remember the (demo) purposes? Definitely applies here as well. Nothing is stopping you from using any other CSS magic of choice.

As mentioned, all components are documented with Storybook (v^7.5). There is nothing fancy about the stories or config organization, as I try to keep things clear, unopinionated and ready for further tweaking.

Here's a slightly reduced example of the Switch stories file:

```typescript
import type { Meta, StoryObj } from '@storybook/react';

import Switch from './Switch';

export default {
  title: 'Form/Switch',
  component: Switch,
} satisfies Meta<typeof Switch>;

type Story = StoryObj<typeof Switch>;

export const Base: Story = {
  args: {},
};

export const Checked: Story = {
  args: {
    ...Base.args,
    defaultChecked: true,
  },
};

export const ColorPrimary: Story = {
  args: {
    ...Base.args,
    color: 'primary',
  },
};
```

As we'll see further, more complex examples using templates and `render:` are also supported, so it's not limited to plain args.

## The Docs

### Overview

Now let's explore the `docs` app. In the project I'm using Astro v^4.0, and at the time of writing it's already updated to 4.4, however no breaking changes are expected, so feel free to use the latest version.

In a nutshell, resulting app consists of the home page, couple of content sections - **Core** and **Patterns**, each containing an introduction and several inner pages, and the Components section, hosting static and dynamic documentation for the UI library.

App is structured in the following way:

```plaintext
src/
	client
	components
	content
	data
	layouts
	pages
	shared
	store
```

* *client* contains the *client:only* components, i.e. `CopyButton`
    
* *components* comprises traditional Astro building blocks
    
* *content* is the required element of the [Astro Collections](https://docs.astro.build/en/reference/api-reference/#content-collections-astrocontent), structured and filled in accordance with the navigation and website contents
    
* *data* contains some additional MD(x) files for the section pages
    
* *layout* and *pages* are standard and self-explanatory
    
* *shared* hosts various utils and hooks
    
* *store* is the optional state management layer, powered by [nanostores](https://github.com/nanostores/nanostores)
    

### Collections

It is worth exploring collections and respective navigation for a little bit, as there is quite a high chance this will be expanded and customized. Luckily, it is actually quite simple.

The easiest way to get familiar with the navigation structure is to check out `shared/globals.ts` - there are a couple of constants that work in sync with the collections. For example the "Core" section needs the following:

```plaintext
// collection of documents
content/core/*.md

// shared/globals.ts
NAVIGATION_KEYS.core = 'core';
NAVIGATION[0] = { slug: NAVIGATION_KEYS.core, title: 'Core' }];
```

And that's it for the basic collections!  
Now, for the most interesting and challenging part.

### Components

Components documentation is basically split into "static" and "dynamic" parts. "Static" part is imported from the dedicated content collection, whereas the "dynamic" part is loaded directly from the UI library.

The easiest way to follow how this alliance works is to start with the `pages/components/[...slug].astro`.  
Here's the part we need:

```typescript
// pages/components/[...slug].astro excerpt

---
const { entry } = Astro.props;

stories = await import(`../../../../../packages/awsm-ui/src/components/${entry.data.dir}/${entry.data.component}.stories.tsx`);
examplesData = refineStories(stories);
---

<Examples 
  data={examplesData} 
  name={entry.data.component} 
  dir={entry.data.dir} 
/>
```

**First**, we get the current Component entry data, defined in the frontmatter (we'll get back to that later).  
**Second** - we [dynamically import](https://vitejs.dev/guide/features.html#dynamic-import) and pass the stories data to the Examples component.

Note, that it is a *server* call and this data will be used further for rendering "slots" with names and other meta information in `Examples`:

```typescript
// components/Examples.astro excerpt

---
const { data, name, dir } = Astro.props;
---

{data.map(([exampleName]) => {
  return (
    <Example heading={exampleName}>
      <ExampleContent
        client:only
        name={exampleName}
        componentName={name}
        componentDir={dir}
      />
    </Example>
  )
})}
```

Evidently, it's a simple presentational component. The one we are actually looking for is `ExampleContent` - let's dive in for a closer look.

`ExampleContent` is meant to be loaded on the client, this is where the dynamic heavy lifting happens. What we need from the stories are in fact the *stories* themselves - most typically the component variants, rendered with the different props.

> 💡 Everything that we need for successful rendering can be found within the stories. Depending on the story format we might need different data.

```typescript
// client/ExampleContent.tsx excerpt

const ExampleContent = (props: ExampleContentProps) => {
  const { name, componentName, componentDir } = props;

  const [struct, data, isLoading] = useStoryLoad(name, componentName, componentDir);

  const { component: AnyComponent } = struct;
  const Component = AnyComponent as JSX.ElementType;

  let Variant;

  if (data.render) {
    if (data.args) {
      const Template = data.render as JSX.ElementType;

      Variant = () => <Template {...data.args} />;
    } else {
      Variant = data.render;
    }
  } else if (Component) {
    Variant = () => <Component {...data.args} />;
  }

  return <Variant />;
}
```

Here, `struct` is an arbitrary term for the `default` object and it's contents look somewhat like this:

```json
default: {
  title: 'Form/Button',
  component: {
    '$$typeof': Symbol(react.forward_ref),
    render: [Function: Button]
  },
  parameters: { layout: 'centered', docs: [Object] },
  tags: [ 'autodocs' ]
},
```

Naturally, having Variant data we can render the source code if needed.

Finally, let's have a look at the loading hook, however you won't find anything exceptional there, as it's pretty standard and predictable at this point. Note that `struct` is not mentioned here, as it's almost identical to the `name` import.

```typescript
// shared/hooks/useStoryLoad.ts excerpt

export const useStoryLoad = (
  name: string,
  componentName: string,
  dir: string | undefined
): [ComponentStruct, Story, boolean] => {
  const componentDir = !!dir ? dir : componentName;

  const [data, setData] = useState<Story>({});

  useEffect(() => {
    async function load() {
      const { [name]: _data } = await import(
        `../../../../../packages/awsm-ui/src/components/${componentDir}/${componentName}.stories.tsx`
      );

      setIsLoading(false);
      setData(_data);
    }

    load();
  }, []);

  return [struct, data, isLoading];
};
```

### Binding

We explored the main mechanism behind component examples rendering, and now it's time to get back to the documentation part.

Components collection represent the static part of the documentation and organized in a particular way:

```plaintext
content/
  components/
    <ComponentName>/
      index.mdx
      [usage.mdx]
    ...
```

`index.mdx` contains the introductory information about component, whereas `usage.mdx` is optional and may contain the docs about component application. If you wish to extend this structure with any custom pages, i.e. "Accessibility", it's easy to do. Check out the following files and adjust data accordingly:

* `shared/globals.ts` - update `COMPONENT_NAVIGATION`
    
* `components/Sidebar.astro` - look for the `currentSlug`
    

What is important about both files is the **frontmatter data**, usage of which you could already notice in the `pages/components/[...slug].astro` file. We can define the schema ourselves and should follow it in all documented components:

```yaml
---
title: 'Button'
page: 'Examples'
group: 'Form'
component: 'Button'
dir: 'Button'
---
```

Here:

* `title` - is the name of the component in the docs
    
* `page` - is the name of the tab in the same docs
    
* `group` - is the custom arbitrary grouping of components; see the usage in the `components/Sidebar.astro`
    
* `component` and `dir` is what we actually use for the stories import
    

### Gimmick

There's one more thing. It's more of a 🎁 bonus, actually.

![Just one more thing...](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2vbddzaojenkrxgt89j1.gif)

Since we rely on the dynamic imports, Astro and Vite will automatically update components on changes - both components' and stories' code! It can give you extra flexibility on documenting (and sometimes *updating*) components, especially if you decide to output them in different example containers.

## Conclusion

AWSM Docs presents a pragmatic approach to UI library or Design System documentation website development. While not claiming to reinvent the wheel, this solution aims to speed up and streamline the process. As with any tool, there's room for improvement, and I encourage further exploration.

It's worth noting that at the time of writing there's an *upgraded and enhanced* version of `awsm-docs` being prepared, stay tuned for another article!

Check out the project on [GitHub](https://github.com/XOP/awsm-docs) and tailor the solution to your specific needs. Your insights and contributions will undoubtedly contribute to the evolution of this approach.

Happy coding and documenting!