microCMS

UIライブラリの「機能」は欲しいけど「見た目」はカスタマイズしたいを叶えるRadix UI

この記事は公開後、1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

フロントエンド開発をしている中でUIライブラリを用いて開発することってありますよね。
機能や見た目、A11Yへの配慮など色々な役割を担ってくれますが、その中でも見た目に関しては独自で定義したいことが多いのではないでしょうか。

そんな時に便利な「Radix UI」というヘッドレスUIの1つをご紹介できればと思います!

ヘッドレスUIについて

RadixUIについて紹介する前にヘッドレスUIについて少し説明させてください。

ヘッドレスUIとはUI要素やインタラクションのロジック、状態、処理、APIを提供し、マークアップやスタイルを提供しないライブラリやユーティリティを指す言葉です。
そのためプロダクトごとのデザインやブランドカラーと完全に合うようにカスタマイズして採用することができます。

そして、ヘッドレスUIと言われるライブラリにはcomponentベースとhooksベースがあります。
componentベースはその名の通りコンポーネントで提供されていて、ユーザーはそのコンポーネントのpropsを指定する形でカスタマイズします。
hooksベースはReactのカスタムフックの形で使用して、それぞれの要素に必要なpropsを受け取り、ユーザーはjsxを自分で組み立てる形となります。
前者はコードの見た目がある程度スッキリするのに対し、後者は少し複雑に見える分、より柔軟に利用できます。

componentベースで有名なものとしては今回紹介するRadix UIの他に、Tailwind LabsのHeadless UIがあります。
hooksベースではAdobeのReact AriaDownshiftがあります。

これらは使い方や揃っている要素の種類などに違いはありますが、基本的には「見た目を独自に当てられてかつ、それ以外の機能面を担ってほしい」という同様の課題を達成できるものです。

Radix UIについて

Radix UIはcomponentベースで提供されるヘッドレスUIです。

公式ドキュメントのIntroductionを参照します。

Radix Primitives is a low-level UI component library with a focus on accessibility, customization and developer experience. You can use these components either as the base layer of your design system, or adopt them incrementally.

※公式からの引用
Radix Primitivesは、アクセシビリティ、カスタマイズ性、開発者の使いやすさを重視した低レベルのUIコンポーネントライブラリです。これらのコンポーネントは、デザインシステムのベースレイヤーとして使用することも、段階的に採用することも可能です。

このように基本的にはヘッドレスUIとして実装されているのに加えて、Radix UIはAPI設計が統一されているのでそれぞれのコンポーネントでギャップを感じることなく、開発者体験にも気を配っています。
また、VisuallyHiddenAccessibleIcon という便利なコンポーネントが用意されているのも大きなポイントです。
たとえばcomponentベースで有名な 「Headless UI」 と比較すると用意されているコンポーネントの種類が多いのとそれぞれのコンポーネントを独立してインストールできるので徐々に採用していきやすい点が挙げられます。
(「Headless UI」はReact以外にもVueでも実装されていたり、Tailwind CSSとの統合がしやすいように設計されていたり別のメリットがありますがここでは割愛します)

使用方法

使いたいコンポーネントをインストールします。
ここでは Popover を例に用います。

yarn add @radix-ui/react-popover

次にインストールしたモジュールからパーツをインポートして使っていきます。
<Popover.Root>のような形で実装に必要なさまざまなパーツを適宜使用して組み立てていきます。

// index.jsx
import * as React from 'React';
import * as Popover from '@radix-ui/react-popover';

const PopoverDemo = () => (
  <Popover.Root>
    <Popover.Trigger>More info</Popover.Trigger>
    <Popover.Portal>
      <Popover.Content>
        Some more info…
        <Popover.Arrow />
      </Popover.Content>
    </Popover.Portal>
  </Popover.Root>
);

export default PopoverDemo;

最後に各々の手法でスタイルを定義すれば完了です。

// index.jsx
import * as React from 'React';
import * as Popover from '@radix-ui/react-popover';
import './styles.css';

const PopoverDemo = () => (
  <Popover.Root>
    <Popover.Trigger className="PopoverTrigger">Show info</Popover.Trigger>
    <Popover.Portal>
      <Popover.Content className="PopoverContent">
        Some content
        <Popover.Arrow className="PopoverArrow" />
      </Popover.Content>
    </Popover.Portal>
  </Popover.Root>
);

export default PopoverDemo;


// styles.css
.PopoverTrigger {
  background-color: white;
  border-radius: 4px;
}

.PopoverContent {
  border-radius: 4px;
  padding: 20px;
  width: 260px;
  background-color: white;
}

.PopoverArrow {
  fill: white;
}

※引用元 https://www.radix-ui.com/docs/primitives/overview/getting-started

microCMSで使用している箇所

microCMSでは現在、権限管理画面のセレクトフィールドとメンバーを選択するUIにて使用しています。

権限管理画面


このセレクトフィールドは @radix-ui/react-select を使用しています。
項目の説明を追加する必要があり、デフォルトのセレクトフィールドのスタイルだと不可能なため、独自のスタイルを当てつつA11Yや標準機能と同様の機能を提供するためRadix UIを利用しました。

またここでは、元々想定していなかったような必要な機能もついてきたのはいい意味で誤算でした!
というのもセレクトフィールドの選択肢が多い場合など画面外にはみ出てしまうような時は、自動的にスクロールされるようになりそのためのボタンみたいなものも追加できました。

以下のようなスクロールが必要になるケースです。


自分だけで実装していると少なからずどこかに見落としが発生しがちだと思いますが、A11Yに関してはそもそもの網羅すべき情報が多いので、こういったライブラリを頼るのは特に有効と考えています。(ある意味で本質の実装に時間をかけられる)

参考までにmicroCMSのカスタムセレクトフィールドのソースをお見せします。
現状は全てのページで使用するコンポーネントではなく、権限管理のページのみで使うコンポーネントとして設計しているため、propsは最小限のものしか指定できない形をとっています。

import * as RadixUISelect from '@radix-ui/react-select';

import styles from './select.module.css';

export type Props = {
  value: string;
  onValueChange(value: string): void;
  name: string;
  disabled?: boolean;
  items: {
    value: string;
    label: string;
    description?: string;
  }[];
};

export const Select: React.FC<Props> = ({
  value,
  onValueChange,
  name,
  disabled,
  items,
}) => {
  return (
    <RadixUISelect.Root
      value={value}
      onValueChange={onValueChange}
      name={name}
      disabled={disabled}
    >
      <RadixUISelect.Trigger className={styles.trigger}>
        <RadixUISelect.Value />
        <RadixUISelect.Icon className={styles.icon}>
          <span className="material-icons-outlined">keyboard_arrow_down</span>
        </RadixUISelect.Icon>
      </RadixUISelect.Trigger>

      <RadixUISelect.Portal>
        <RadixUISelect.Content className={styles.content}>
          <RadixUISelect.ScrollUpButton className={styles.scrollButton}>
            <span className="material-icons-outlined">keyboard_arrow_up</span>
          </RadixUISelect.ScrollUpButton>
          <RadixUISelect.Viewport className={styles.viewport}>
            {items.map(({ value, label, description }) => (
              <RadixUISelect.Item
                key={value}
                value={value}
                className={styles.item}
              >
                <RadixUISelect.ItemText>{label}</RadixUISelect.ItemText>
                {description && (
                  <p className={styles.description}>{description}</p>
                )}
              </RadixUISelect.Item>
            ))}
          </RadixUISelect.Viewport>
          <RadixUISelect.ScrollDownButton className={styles.scrollButton}>
            <span className="material-icons-outlined">keyboard_arrow_down</span>
          </RadixUISelect.ScrollDownButton>
        </RadixUISelect.Content>
      </RadixUISelect.Portal>
    </RadixUISelect.Root>
  );
};

※一部省略しています

このように、A11Yに必要な属性やキーボード操作のために必要なロジックなどがライブラリ側に隠蔽され、とてもスッキリした実装になっています。
ちなみにここで開かれるセレクトフィールドは内部的にReactの createPortal を使用してbodyの直下に描画されるため、 いわゆる重なり順の問題もほぼ起きません。
そして、デフォルトのセレクトフィールドの動作に合わせて、ボタンの近くに表示されるようになっているのもありがたいところです。

レビュー申請画面のレビュアー指定


ここでは @radix-ui/react-popover を使用しています。
元々は独自に構築されていたのですが、メンバーの数が多かったり、ウィンドウサイズの縦幅が小さい時にはみ出てしまい、正常にメンバーを選択することができない時がありました。
また、キーボードのフォーカス管理なども課題感がありましたので、それらを改善するためこのコンポーネントにおいても導入しました。

ただ、残る課題としてinputタグとリストが組み合わさったようなcomboboxと言われるUIの最適な実装はまだできていません。
これは https://www.w3.org/TR/wai-aria-1.1/#combobox に記載のある通りには実装できていなく、Radix UIにも現時点ではcomboboxのコンポーネントが用意されていないため、一旦保留としている状況です。(自分で実装する手もあるかと思いますがめっちゃむずそうなので後回し気味です💦)

最後に

microCMSでは少しずつですがA11Y改善にも励んでいます。
特にキーボード操作の面では滑らかな操作感を実現するためにも、これから改善していきたいです!

Radix UIのようなヘッドレスUIは、影響範囲も少なく済むことが多く導入のハードルが低いのに対し、メリットが大きくとてもおすすめですのでぜひ試してみてください!

-----

microCMSは日々改善を進めています。
ご意見・ご要望は管理画面右下のチャット、公式Twitterお問い合わせからお気軽にご連絡ください!
引き続きmicroCMSをよろしくお願いいたします!

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

千葉大輔
microCMSでプロダクトエンジニアをしています。Twitterでは「でぃーすけ」という名前で活動しており、ReactやNext.js、TypeScript、TailwindCSSが特に好きです。趣味はゲームやったりアニメ見たり料理したりをループしています。