005

Stitches & Storybook Integration

| ~4 min read

I had recently started a new project and didn't have any hard requirements about styling solutions, so I needed to pick something. I decided to go with the Stitches library, which I'd been eyeing for a while. This post isn't my review but here's a short list of my initial thoughts in case you're curious:

Pros:

  • Fantastic DX, it's really pleasant to work with
  • Once you understand the concept of the theme, scales, & tokens, it's really intuitive and productive
  • Architecturally, I think it's great. As a consumer you have a lot of control but it will stay out of your way if you want too. You don't have to endlessly tinker to get to working on your site's problems.

Cons:

  • Still young, it isn't as mature as some other approaches so you may run into oddities or a less than ideal compatibility story with common tooling (keep reading!)
  • Similar to previous, but it has a smaller user base which means there isn't as much published content out there to help with debugging issues.
  • Due to it's feature set and flexibility some of it's typings are complex and hard to approach I think.

One issue I ran into was having Storybook & Stitches play nice with each other. This is sad because, in my mind, they are both component-driven tooling that have a strong focus on DX.

This is actually quite common, I've been lurking in the Stitches discord and have seen this question come up a number of times in just a month. There are some old tickets and discussions that I took and modified for this solution.

The long and short of it is that Stitches' Styled component's as & css props are too flexible for Storybook to make sense of and just "do the right thing" when rendering controls for the variants and props of a component.

Here is a solution that will help you pass Storybook the proper type so you get a much more friendly interface. I put it in a gist to help with the copy/pasting/forking...

It is basically just a utility that helps shake out the Stitches props we don't want.

// File: type-utils.ts
import type * as Stitches from "@stitches/react";
interface StitchesMedia {
[x: string]: any;
// Add/remove what you want to omit from SB, we're using this currently
initial?: any;
as?: any;
css?: Stitches.CSS;
}
// We exclude these type properties from the `ComponentVariants` type so that storybook can more
// easily understand the type arguments. We exclude `"true"` and `"false"` strings as well since
// stitches also adds these, and they aren't necessary for storybook controls.
type StitchesPropsToExclude = "true" | "false" | StitchesMedia;
export function modifyVariantsForStory<ComponentVariants, ComponentProps, ComponentType>(
component: ((props: ComponentProps) => JSX.Element) | ComponentType
) {
type ComponentStoryVariants = {
[Property in keyof ComponentVariants]: Exclude<
ComponentVariants[Property],
StitchesPropsToExclude
>;
};
type ComponentStoryProps = Omit<ComponentProps, keyof ComponentVariants> &
ComponentStoryVariants;
return component as unknown as (props: ComponentStoryProps) => JSX.Element;
}

And then, you can use it like so:

// File: Example.tsx
import type * as Stitches from "@stitches/react";
import { modifyVariantsForStory } from "type-utils";
import { styled } from "../stitches.config";
const Example = styles("div", {
// ... Styles & Variants
});
export default Example;
// Only export/use these in Storybook since they are just for the Stitchs x SB handshake
// Can be here or in the story
type ExampleVariants = Stitches.VariantProps<typeof Example>;
interface ExampleProps extends ExampleVariants {}
// Use this as the type in Story; i.e. `ComponentMeta<typeof ButtonStory>`
export const ExampleStory = modifyVariantsForStory<ExampleVariants, ExampleProps, typeof Example>(Example);

And in your component's story:

// File: Example.stories.tsx
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import Example, { ExampleStory } from './Example';
export default {
title: "Example",
component: Example
} as ComponentMeta<typeof ExampleStory>;
const Template: ComponentStory<typeof ExampleStory> = (args) => (<Example { ...args } />);
export const Default = Template.bind({});
Default.args = {
children: "Example usage"
};

And that's it, Storybook should now properly interpret the variants and properly render knobs accordingly for your component. Your mileage may vary, but I hope it helps you - good luck!