こんにちは。 先日、ちょっとしたきっかけでStorybookのアドオンをはじめて開発しました。 本記事では、そのStorybookのアドオン開発の体験を共有したいと思います。
storybook-addon-range-controls というアドオンを開発しました。 npmで公開しています。
このアドオンは、端的にいうと Storybook上で、コンポーネントの文字列・数値・配列の引数を、スライダーでデータ増減できるアドオン です。 作ろうと思ったきっかけは、StorybookのArgTypesでrangeの値を使って、データ増減に伴うデザイン崩れをチェック していたのですが、 配列のスライダーをするのに少しだけコードを書く手間があり、もっと簡単にできないかというのがきっかけです。
デモは、以下のリンクで公開していますので、気になる方はぜひ触ってみてください。
このアドオンを作るために、何をしたのかを振り返ります。
実装したコードベースは、以下で公開していますので、よければご参考ください。
まず、Storybookのアドオンってどうやって作るのか、作った経験がない私にはわからなかったので、 以下のStorybookのドキュメントを読みました。
読んでいると、以下のアドオンキットがあることに辿り着きました。
これは、Storybookのアドオン開発に必要なものが最低限揃っていて、最初のとっかかりにはちょうどよかったです。
アドオンキットを読んでいると、以下の3つの種類のアドオン開発があることを知りました。
3つの種類についての説明は、以下の公式ページがわかりやすいです。
今回、私がなんとなく想像していたのはパネルだったので、今回はパネルを使ってみることにしました。
アドオンキットでは、開発するアドオンを組み込んだStorybookを起動できる(npm run start)ようになっていて、起動させたStorybook上でアドオンの実装を進めていきます。
アドオン開発には、さまざまなAPIが用意されており、公式のドキュメントが参考になります。
例えば、表示しているStoryのparamtersやargsを取得するhooksなどがあります。 以下は、私が利用したhooksのコード例です。
import React, { memo } from "react";
import type { RangeControlsParameters } from "src/types";
import { useParameter, useArgs } from "storybook/manager-api";
import { useTheme } from "storybook/theming";
import { KEY } from "../../constants";
type Props = {
active: boolean;
};
export const Panel = memo((props: Props) => {
const config = useParameter<RangeControlsParameters>(KEY, {});
const theme = useTheme();
const [args, updateArgs] = useArgs();
return (<>...</>);
});また、パネルのUI開発には、以下にあるように、コンポーネントやスタイルなどが用意されています。
上記のコンポーネントやスタイルにあるタイポグラフィやカラー、スペーシングなどを使うだけなので、 ゼロから作り上げるということはありません。また、ダークテーマの対応もできます。
以下は、私が実装したパネルのコード例です。 emotionを使ったことがある人なら、理解しやすいコードかと思います。
// PropControl.styles.ts
import { Badge } from "storybook/internal/components";
import { styled, typography } from "storybook/theming";
export const StyledDetails = styled.details`
border: 1px solid
${({ theme }) =>
theme.base === "dark" ? theme.color.dark : theme.color.border};
margin-bottom: ${({ theme }) => theme.layoutMargin}px;
background: ${({ theme }) => theme.color.lightest};
`;
export const StyledSummary = styled.summary`
padding: ${({ theme }) => theme.layoutMargin}px;
cursor: pointer;
font-weight: ${typography.weight.bold};
font-size: ${typography.size.s2}px;
display: flex;
align-items: center;
justify-content: flex-start;
color: ${({ theme }) => theme.color.defaultText};
background: ${({ theme }) => theme.background.content};
&:hover {
background: ${({ theme }) =>
theme.base === "dark" ? theme.color.darker : theme.color.lighter};
}
`;
export const SummaryBadge = styled(Badge)`
margin-left: auto;
`;npmへの公開は、アドオンキットにあるGitHub Actionsの release.yml をほとんどそのまま使いました。 必要なAPIトークンは、READMEに書いてあるので、その通りにやれば大丈夫です。
アドオンの使い方を示すためにデモが欲しかったので、Chromaticのサービスを使用しました。
以下のURLにデモを公開しています。
以下のaddon-kitのissueにもあるように、ホットリロードがうまく動かないことがありました。
そのため、開発中はコード修正してリロードを繰り返していました。ちょっと面倒でしたね。
StorybookのParametersに関数を設定したとしても、useParameterでは関数が取得できません。 JSONシリアライズの関係かと思います。
少し強引なやり方かもしれませんが、プレビューのデコレータからだと、パラメータに関数がまだ残っているため、そのデータをパネルへ渡すようにしました。
以下のように、プレビューのデコレータで、関数入りパラメータを独自シリアライズをし、パネルではそのデータを取得するようにしました。
// withGlobals.ts
import type {
Renderer,
StoryContext,
PartialStoryFn as StoryFunction,
} from "storybook/internal/types";
import { useEffect, useChannel } from "storybook/preview-api";
import { EVENTS, KEY } from "./constants";
import { serializeFunctions } from "./utils/serialize";
export const withGlobals = (
StoryFn: StoryFunction<Renderer>,
context: StoryContext<Renderer>,
) => {
const emit = useChannel({});
useEffect(() => {
const params = context.parameters?.[KEY];
if (params) {
const serialized = JSON.stringify(serializeFunctions(params));
emit(EVENTS.PARAMETERS_SYNC, serialized);
}
}, [context.id, context.parameters]);
return StoryFn();
};// preview.ts
import type { ProjectAnnotations, Renderer } from "storybook/internal/types";
import { KEY } from "./constants";
import { withGlobals } from "./withGlobals";
const preview: ProjectAnnotations<Renderer> = {
decorators: [withGlobals],
initialGlobals: {
[KEY]: false,
},
};
export default preview;// Panel.tsx
import React, { memo } from "react";
import type { RangeControlsParameters } from "src/types";
import { useChannel } from "storybook/manager-api";
import { EVENTS } from "../../constants";
import { reviveFunctions } from "../../utils/serialize";
type Props = {
active: boolean;
};
export const Panel = memo((props: Props) => {
useChannel({
[EVENTS.PARAMETERS_SYNC]: (serialized: string) => {
try {
const parsed = JSON.parse(serialized);
const revived = reviveFunctions<RangeControlsParameters>(parsed);
// Do something with the revived parameters
} catch (e) {
console.error("Failed to deserialize parameters", e);
}
},
});
return (<>...</>);
});Storybookのアドオン開発って、どれぐらい難しいのかなと思ったのですが、 アドオンキットのおかげで、思ったよりも簡単に開発できました。 コンポーネントやスタイルも整っているし、リリースも簡単にできましたし、素晴らしいエコシステムだなと思います。
タグ「フロントエンド」の記事
最近、ヒューマンインターフェース ガイドライン(HIG)という言葉を知りました。 「ヒューマンインターフェイスガイドライン」には、どのAppleプラットフォームでも優れた体験を設計できるようにするためのガイドとベストプラクティスが含まれてい
2026-01-24
主にWeb関連の個人開発をしている際に心がけていることを書きます。 月末に近づくにつれ、AIの利用上限に達してしまうことがあります。 その状況になった時、以下のいずれかの選択肢が私の中では残っています。 課金して利用上限を増やす 無料モデル
個人サイトをリニューアルをしています。 ノート風のデザインを目指して、スタイルを調整していました。 ノートの見た目は、現実にあるノートを再現しようとCSSを書いていました。 現在、以下の画像のようなノートになっています。 ノート風デザインの
2026-01-20