Storybook
Storybook — это инструмент для разработки и документирования UI-компонентов. Он создаёт
изолированную среду, где можно просматривать, тестировать и изменять интерфейсные компоненты вне зависимости от
основного приложения. Storybook поддерживает различные библиотеки, такие как React, Vue, Angular и другие, и может
быть полезен для команд разработки, дизайнеров и тестировщиков.
Основные функции Storybook
-
Изоляция компонентов: компоненты отображаются в изолированном окружении, что позволяет избежать влияния других элементов приложения. Это удобно для разработки и отладки компонентов.
-
Документация: каждый компонент может сопровождаться документированными историями, что облегчает его понимание, использование и поддержку другими членами команды.
Storybookсоздаёт живую документацию, где можно увидеть компоненты в действии с разными параметрами (props). -
Тестирование интерфейсов:
Storybookлегко интегрируется с инструментами для визуального тестирования, такими как Chromatic, что позволяет отслеживать изменения в интерфейсе и предотвращать визуальные баги. -
Поддержка аддонов:
Storybookподдерживает различные аддоны, которые позволяют добавлять функциональность (например, контроль доступности, тестирование взаимодействия и локализации). -
Консистентность интерфейса: разработчики и дизайнеры могут просматривать все компоненты в одном месте, что помогает соблюдать единый стиль и стандарты в проекте.
В общем, Storybook упрощает процесс разработки интерфейсов, позволяет быстро демонстрировать UI-компоненты, а также
служит интерактивной документацией для команды.
Примеры для вдохновления:
Stories
Устанавливаем и стартуем проект согласно документации
pnpm dlx storybook@latest initРезультат. Запустим storybook и посмотрим визуально, что у мы имеем из коробки 🚀
Button
Пробежавшись по коду и визуально поняв как это выглядит давайте напишем свою первую историю для компонента Button.tsx
Заглушка
Вот это стандартная заглушка, которую вы будете использовать для всех историй. Только менять Button на нужный компонент
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'
const meta = {
component: Button,
} satisfies Meta<typeof Button>
export default meta
type Story = StoryObj<typeof meta>Satisfies
Оператор satisfies — это новая конструкция, добавленная в версии 4.9, которая помогает указать, что объект удовлетворяет
определённому типу. Она обеспечивает более точное соответствие между объектом и типом, чем простая аннотация типов, и
может выявлять лишние или несовместимые поля.
Первая история
Мы просто описываем вариант компоненты, который хотим увидеть
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
}Получаем следующий результат

История отобразилась, но стилей нет 🥲
Для того чтобы в storybook применялись стили их необходимо добавить в .storybook/preview.ts
import type { Preview } from '@storybook/react'
import '../src/index.css'
/*...*/
Результат. Первая история готова 🚀
Как писать истории
Историй описывающих компонент может быть сколько угодно. В истории описываются каким может быть компонент, различные его состояния, чтобы человек который смотрит сторибук сразу понимал что умеет ваш компонент и как его применять
Написание историй вручную
Просто копируем первую историю, меняем variant и получаем еще одну историю 🚀
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
}
export const LinkAsButton: Story = {
args: {
children: <a href="/sign-up">Sign Up</a>,
asChild: true,
},
}Написание историй в addons панели
Если по умолчанию не открыта addons панель нажмите ALT + A
Меняем в панель состояние кнопки, нажимаем кнопку Crete new story

Даем название для истории

И история создастся автоматически (откройте Button.stories.tsx и убедитесь в этом) 🚀
Документация
Для того чтобы автоматически сгенерировать документацию необходимо добавить tags: ['autodocs'] в meta
const meta = {
component: Button,
tags: ['autodocs'],
} satisfies Meta<typeof Button>Результат. Получаем красивую документацию, описывающую различные варианты компонента 🚀

Расширенная документация
Storybook предоставляет множество решений для написаний документации. Чтобы автоматически генерировать документации,
намо просто описать при помощи JSDoc комментариев.
Давайте подсмотрим как сделана документация на примере кнопки, которую автоматически создал сторибук при его установке.
export interface ButtonProps {
/** Is this the principal call to action on the page? */
primary?: boolean
/** What background color to use */
backgroundColor?: string
/** How large should the button be? */
size?: 'small' | 'medium' | 'large'
/** Button contents */
label: string
/** Optional click handler */
onClick?: () => void
}
/** Primary UI component for user interaction */
export const Button = () => {
/*...*/
}Сделаем по аналогии для нашей кнопки
type Props = {
/** Choose from 3 style variants. Default: 'primary'. */
variant?: 'primary' | 'secondary' | 'outlined'
/** Render the Button using any element if asChild true */
asChild?: boolean
} & ComponentPropsWithoutRef<'button'>
/** Ui kit Button component */
export const Button = ({ variant = 'primary', className, asChild, ...rest }: Props) => {
const Component = asChild ? Slot : 'button'
return <Component className={clsx(s.button, s[variant], className)} {...rest} />
}/** Primary variant. Used as 'default'*/
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
}
/** Secondary variant*/
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
}
/** Link as Button variant*/
export const LinkAsButton: Story = {
args: {
children: <a href="/sign-up">Sign Up</a>,
asChild: true,
},
}
/** Outlined variant*/
export const Outlined: Story = {
args: {
variant: 'outlined',
children: 'Outlined Button',
},
}Результат. Документация готова 🚀

Modal
Реализуем по аналогии ModalRadix.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { ModalRadix } from './ModalRadix'
const meta = {
component: ModalRadix,
tags: ['autodocs'],
} satisfies Meta<typeof ModalRadix>
export default meta
type Story = StoryObj<typeof meta>
export const BaseModal: Story = {
args: {
open: true,
onClose: () => console.log('modal close'),
modalTitle: 'Modal title',
children: (
<div>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab at cumque distinctio dolorem
doloremque impedit incidunt, inventore iure officia omnis placeat possimus praesentium,
quisquam repellendus repudiandae saepe sunt ullam veniam.
</div>
),
},
}type ModalSize = 'lg' | 'md' | 'sm'
type Props = {
/** The controlled open state of the Modal */
open: boolean
/** Close modal handler */
onClose: () => void
/** Modal title */
modalTitle: string
/** 'sm' | 'md' | 'lg':
* sm - 367px,
* md - 532px,
* lg - 764px.
* Default: 'md'
* For other values use className */
size?: ModalSize
} & ComponentPropsWithoutRef<'div'>
/** Ui kit ModalRadix component */
export const ModalRadix = ({
size = 'md',
open,
modalTitle,
onClose,
children,
className,
...rest
}: Props) => {
/*...*/
}Результат. Базовая история для модалки реализована 🚀

Working with React Hooks
Но есть один нюанс: модалка сразу открыта, а хотелось бы по нажатию проверять, что она отрабатывает как необходимо. Для того чтобы это реализовать пойдем в документацию.
React Hooks — это удобные вспомогательные методы для создания компонентов с более упрощённым подходом. Вы можете использовать их при создании историй для вашего компонента, если это необходимо, хотя стоит рассматривать их как продвинутый случай использования. Мы рекомендуем использовать аргументы по возможности при написании собственных историй
import { Meta, StoryObj } from "@storybook/react"
import { useState } from "react"
import { Button } from "../Button/Button.tsx"
import { ModalRadix } from "./ModalRadix.tsx"
const meta = {
component: ModalRadix,
tags: ["autodocs"],
} satisfies Meta<typeof ModalRadix>
export default meta
type Story = StoryObj<typeof ModalRadix>
/** BaseModal */
export const BaseModal: Story = {
args: {
modalTitle: "Modal title",
children: <div>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cupiditate, error.</div>,
},
render: (args) => {
const [showModal, setShowModal] = useState(false)
const openModalHandler = () => {
setShowModal(true)
}
const closeModalHandler = () => {
setShowModal(false)
}
return (
<>
<Button variant={"primary"} onClick={openModalHandler}>
Open modal
</Button>
<ModalRadix {...args} open={showModal} onClose={closeModalHandler} />
</>
)
},
}
Если будет eslint ошибка, то устранить ее можно следующим образом
Результат. Теперь все работает и ошибок нету 🚀

Small modal
Реализуем новую историю для модального окна при котором измени размер модального окна (size='sm')
/** SmallModal */
export const SmallModal: Story = {
args: {
...BaseModal.args,
size: "sm",
},
render: (args) => {
const [showModal, setShowModal] = useState(false)
const openModalHandler = () => {
setShowModal(true)
}
const closeModalHandler = () => {
setShowModal(false)
}
return (
<>
<Button variant={"primary"} onClick={openModalHandler}>
Open modal
</Button>
<ModalRadix {...args} open={showModal} onClose={closeModalHandler} />
</>
)
},
}Результат. История работает, но дублирование кода на лицо
Избавляемся от дублирования
Для того, чтобы не дублировать код вынесем код в отдельный компонент, назовем его Render (можно как угодно) и
передадим его в свойство render.
Для типизации args:
-
переименуем и
PropsнаModalRadixPropsв компонентеModalRadix.tsx -
экспортируем
ModalRadixProps
export type ModalRadixProps = {
/*...*/
} & ComponentPropsWithoutRef<"div">function Render(args: ModalRadixProps) {
const [showModal, setShowModal] = useState(false)
const openModalHandler = () => {
setShowModal(true)
}
const closeModalHandler = () => {
setShowModal(false)
}
return (
<>
<Button variant={"primary"} onClick={openModalHandler}>
Open modal
</Button>
<ModalRadix {...args} open={showModal} onClose={closeModalHandler} />
</>
)
}
/** BaseModal */
export const BaseModal: Story = {
args: {
modalTitle: "Modal title",
children: <div>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cupiditate, error.</div>,
},
render: Render,
}
/** SmallModal */
export const SmallModal: Story = {
args: {
...BaseModal.args,
size: "sm",
},
render: Render,
}Результат. Истории отрабатывают как и прежде. Избавились от дублирования кода 🚀