LoginSignup
12
8

RadixUI、Stitches、Storybookを用いてモーダルコンポーネントを作ってみた。

Last updated at Posted at 2023-10-26

始めに

アドベントカレンダーイベント参加を機に、フロントエンド開発におけるUIライブラリの選定とアーキテクチャについて学習するため、モーダルコンポーネントを作成してみました。
まだまだ改善の余地はあると思いますが、出だしとして記事にまとめてみようと思いました。

構成

タイトル 備考   
始めに
1 学習の経緯、技術選定 学習をしようと思った経緯
2 ライブラリとStorybookについて説明 今回使用したライブラリとStorybookについてざっくり説明
3 設計・実装手順 実際の設計、実装手順ついて
4 作ってみた感想 良かったところ
5 最後に

学習の経緯、技術選定

有志でフロントエンド開発におけるUIライブラリの選定とアーキテクチャについて学習する過程で、モーダルコンポーネントを作成してみました。
また、css in jsやヘッドレスUI、Storybookに触れたことが無かったのでこの機会に触れてみようと思った。

選定技術がこちらです。

  • フロントエンド
    • React、TypeScript
  • ライブラリ
    • RadixUI(ヘッドレスUI)
    • Stitches(css in js)
  • その他 
    • storybook

ライブラリとStorybookについて説明

初見なので、Radix UI、Stitches、Storybookについて調べてみました。

Radix UI

Headless UIコンポーネントを提供している。スタイリングを取り除いた、機能だけを利用する際に使用することも可能。
Radixは4つのパッケージに分かれており、今回はRadix Primitivesのドキュメントを参考にモーダルコンポーネントを作成しました。
Radix Prmitivesは、スタイルが全く適用されておらず、スタイルは自分で行い機能だけを利用する際に使うパッケージ。

参考記事

Stitches

css in jsライブラリーの1種。near runtime zeroやSSRにも対応。near runtime zeroについてあまり分かっていないので、これから調べていこうと思います。
tokenやvariantsを使用することが可能で、スタイルコンポーネントを作成する際に便利。
また今回css in jssを触ってみたかったので、下記の記事から上位のcss in jssのライブラリを選択しました。またRadixUIの公式ドキュメントでスタイリングのサンプルがStitchesで書かれていることから選びました。

参考記事

Storybook

UIカタログを作成するツール。手軽にブラウザで確認することができ、コンポーネントごとにドキュメントを作成できる。

参考記事

設計・実装手順

以下手順で、設計、実装してみました。

  • 使用される場面やユースケースを考える
  • 設計(interfaceのprops周りと警告モーダルのイメージ作成)
  • 実装

使用される場面やユースケースを考える

今回はモーダルコンポーネントを作成すると決めたので、モーダルのユースケースを調べました。
警告時に表示されるモーダルのユースケースを想定し、削除ボタンを押すと警告モーダルが表示するケースで実装しました。

参考記事

設計(props、警告モーダルのイメージ作成)

今回はfigmaを利用し、警告モーダルのイメージを作成しました。
そこから今回のモーダルで必要とされる機能を考え、必要なpropsを洗い出しました。

以下がpropsの内容です。

//Propsの型定義
interface Props {
  openButtonLabel: string,
  title: string,
  content: ReactNode,
  cancelButtonLabel?: string,
  okButtonLabel?: string
}

削除ボタンのラベル、警告モーダルのタイトル、内容、閉じるボタン、承認ボタンをPropsのプロパティとして定義。
モーダルのコンテンツは色々想定できるため、今回はReactNodeを使用し様々な型に柔軟に対応できるように一旦実装しました。

ReactNodeについて以下の記事を参照しました。

cancelButtonLabelとokButtonLabelは表示させる内容があまり変わらないことを想定し、コンポーネント内部にデフォルト値をあらかじめ設定しておくことにしました。

export const ModalTest = (
  {
    cancelButtonLabel = '閉じる',
    okButtonLabel = '承認',
    ...props
  }
  :Props
  ) => {
  return()}

実装内容

ModalComponentを作成

モーダルコンポーネントを作成するため、以下のファイルを作成しました。

  • Modal.tsx
    警告モーダルに必要なpropsの型定義、Stitchesを用いたスタイリングコンポーネント、Radixを用いたモーダルコンポーネントを作成。
  • Modal.stories.tsx
    StorybookにModalのドキュメントを作成するための設定、モーダルコンポーネントに渡すpropsの値を設定。

最初はStitchesでスタイリングせず、Radixで作成したモーダルコンポーネントをStorybookにドキュメント化しました。

RadixUIのみで警告モーダルを作成した場合

Modal.tsx


import * as Dialog from '@radix-ui/react-dialog';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
import { ReactNode } from "react";

//Propsの型定義
interface Props {
  openButtonLabel: string,
  title: string,
  content: ReactNode,
  cancelButtonLabel?: string,
  okButtonLabel?: string
}


export const ModalTest = (
  {
    cancelButtonLabel = "No”
    okButtonLabel = "Yes"
    ...props
  }
  :Props
) => {
  return (
    <Dialog.Root>
      <Dialog.Trigger>
        {props.label}
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay>
        <Dialog.Content>
          <Dialog.Title>{props.title}</Dialog.Title>
          <Dialog.Description>{props.content</Dialog.Description>
          <Dialog.Description><ExclamationTriangleIcon/></Dialog.Description>
          <Dialog.Close>{cancelButtonLabel}</Dialog.Close>
          <Dialog.Close>{okButtonLabel}</Dialog.Close>
        </Dialog.Content>
        </Dialog.Overlay>
      </Dialog.Portal>
    </Dialog.Root>
  )
}
Modal.stories.ts

import type { Meta, StoryObj } from '@storybook/react';
import { ModalTest } from './Modal';


const meta = {
  title: 'Example/Modal',
  component: ModalTest,
} satisfies Meta<typeof ModalTest>

export default meta;

type Story = StoryObj<typeof meta>;

export const DeleteModal: Story = {
  args: {
    openButtonLabel: 'Delete',
    title: '削除してもよろしいですか?',
    content: '削除すると戻すことができません!!',
  },
};

結果はこんな感じ。

スクリーンショット 2023-10-25 8.54.14.png

Stitchesで警告モーダルをスタイリングした場合

デフォルトのトークンテーマなどをあらかじめ設定するため、stitches.config.tsを作成。

stitches.config.ts

import { createStitches } from "@stitches/react";
import type * as Stitches from '@stitches/react';

export const { styled } = createStitches(
  {
    theme: {
      colors: {
        red: "#F52147",
        white: "#FFF",
        black:"#000"
      },
      font:{
        defType: "'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;"
      },
      fontSizes:{
        small: "10px",
        medium:"16px",
        large:"25px",
      },
    },
    utils:{
      p: (value: Stitches.PropertyValue<'padding'>) => ({padding: value,}),
      ta: (value: Stitches.PropertyValue<'textAlign'>) => ({ textAlign: value }),
      br: (value: Stitches.PropertyValue<'borderRadius'>) => ({borderRadius: value,}),
      m: (value: Stitches.PropertyValue<'margin'>) => ({margin: value,}),
      size: (value: Stitches.PropertyValue<'width'>) => ({width: value,height: value,}),
    }
  })

各styledコンポーネントを作成し、警告モーダルにスタイリング

Modal.tsx

import * as Dialog from '@radix-ui/react-dialog';
import { styled, Colors} from '../../stitches.config.ts';
import { ReactNode } from "react";
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'


interface Props {
  openButtonLabel: string,
  title: string,
  content: ReactNode,
  cancelButtonLabel?: string,
  okButtonLabel?: string
}

const StyledDialogTrigger = styled(Dialog.Trigger,{
  backgroundColor: "$colors$red",
  fontSize: "$fontSizes$medium",
  font: "$defType",
  p: "5px 6px",
  br: "30px",
})

const StyledDialogOverlay = styled(Dialog.Overlay,{
  backgroundColor: "rgba(0, 0, 0, 0.5)",
  position: "fixed",
  top: 0,
  left: 0,
  right: 0,
  bottom: 0,
  display: "grid",
})

const StyledDialogContent = styled(Dialog.Content,{
  backgroundColor: "$white",
  border:"$colors$black",
  br: "6px",
  ta: "center",
  m: "auto",
  ai:"center",
  p: "30px"
})

const StyledDialogTitle = styled(Dialog.Title,{
  variants: {
    color: {
      red: {color: "$colors$red"}
    },
    fontSize:{
      medium: {fontSize:"$fontSizes$medium"}
    }
  }
})

const StyledDialogDescription = styled(Dialog.Description,{
  fontSize: "17px"
})

const StyeldDialogCloseButton = styled(Dialog.Close,{
  m: "10px"
})

const StyledImage = styled(ExclamationTriangleIcon,{
  size: "50px"
})


export const ModalTest = (
  {
    cancelButtonLabel = '閉じる',
    okButtonLabel = '承認',
    ...props
  }
  :Props
  ) => {
  return (
    <Dialog.Root>
      <StyledDialogTrigger >{props.openButtonLabel}</StyledDialogTrigger>
      <Dialog.Portal>
        <StyledDialogOverlay>
        <StyledDialogContent onPointerDownOutside={(e) => e.preventDefault()}>
          <StyledDialogTitle color="red" fontSize="medium">{props.title}</StyledDialogTitle >
          <Dialog.Description>
            <StyledImage />
          </Dialog.Description>
          <StyledDialogDescription>
            {props.content}
          </StyledDialogDescription>
          <StyeldDialogCloseButton>{cancelButtonLabel}</StyeldDialogCloseButton>
              <StyeldDialogCloseButton>{okButtonLabel}</StyeldDialogCloseButton>
         </StyledDialogContent>
         </StyledDialogOverlay>
       </Dialog.Portal>
    </Dialog.Root>
  )
}

スタイリング後の結果がこちら
スクリーンショット 2023-10-25 18.28.00.png

スクリーンショット 2023-10-25 18.24.14.png

参考文献

作ってみた感想

今回モーダルコンポーネントを作成してみて、個人的には以下の学びがあったかなと思います。

  • UIで見た目と機能で分けられのは便利
    RadixUIのPrimitivesのように、スタイリングは別でできるのは柔軟性があり良いなと思いました。UIライブラリによって自分が思っている成果物と少し違うというところなどを改善してくれるのかなと思いました。
  • tsxファイルにcssを記載できる良さ
    コンポーネント内で完結できるところや、propsを通じてcssを動的に変更ができるので便利だなと思いました。(今回の記事ではまだ対応できていない)
  • StorybookのUIカタログを作成できる
    作成したUIパーツをカタログ形式で確認できるのは良いなと思いました。コミュニティで共有する上で便利ツールだとも思います。

最後に

初めてcss in jsライブラリ、HeadlessUI、Storybookを使用する過程で色々学びがありました。まだ改善の余地が多くあると思うので、今後も継続して学習して行きたいと思いました。
最後まで読んでいただきありがとうございました。

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8