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!