sashigane
コンポーネント

Carousel

カルーセル。横に並べて順に見せる。写真や紹介に使う

依存が2つインストールされます

Carousel を追加すると embla-carousel-react と embla-carousel-autoplay が 一緒にインストールされます。Carousel を使うときだけです。

スクロールの仕組みは Embla が持っています。素の scroll-snap でもスクロールと吸着はできますが、 掴んで引く操作と端で折り返すスクロールは自分で書くことになります。 どちらもサイトやアプリで普通に使われるものです。

Embla の設定をそのまま渡せます

options は Embla の設定をそのまま受けます。 こちらで包み直していないので、 Embla が設定を足せば何もせずに使えます。

<Carousel label="おすすめ" options={{ loop: true, align: 'start', slidesToScroll: 2 }}>

よく使うもの。

loop端で折り返す。順番に意味が無いときだけ(下に条件があります)
align吸着の位置(start / center / end)
slidesToScroll1回で何枚スクロールするか
dragFree吸着せずに慣性で流す
startIndex何枚目から始めるか

全部の一覧は Embla の設定にあります。

duration だけ渡せません。 スクロールの速さは動きを減らす設定(prefers-reduced-motion)から決めているので、 渡せるようにすると利用側がその設定を打ち消せてしまいます。 尊重するのは利用側の責務ではありません。

ブラウザが作ってくれる仕組み(::scroll-button() と ::scroll-marker)は Chromium にしかありません。 乗せると、他のブラウザで操作手段そのものが消えます。

名前が要ります

label は必須です。何のカルーセルかが読み上げに出ないと、 前後にスクロールしても何を見ているのか分かりません。

<Carousel label="おすすめ">

端では押せません

移動先が無いのに押せるのは筋が通りません。 1枚しか無ければ、前も次も押せません。

折り返しをいつ使うか

折り返す形(options={{ loop: true }})は既定ではありません。

入れてよい写真・広告・お客様の声のように、順番に意味が無いもの。どこから見ても構わない
入れない手順・記事の並び・検索結果のように、順番に意味があるもの

折り返すと「最後まで来た」が分からなくなります。 順番に意味があるものでは、終わりに着いたことが読み取れなくなります。

現在位置はスクリーンリーダーにも伝わります

位置表示のボタンには aria-current が付きます。 色の違いだけで示すと、色が見えない利用者や読み上げには届きません。

ボタンと位置表示は、スクロールする枠の外に置いてください

CarouselSlides がスクロールする枠を描きます。その中に置くと一緒にスクロールしてしまい、 2枚目へ進んだ瞬間に画面の外へ出ます。見た目には「ボタンが消えた」としか映りません。

自動で動かすときは、停止ボタンが必須です

autoplay を使うときは CarouselPlayPause を一緒に置いてください。 置かないとエラーになります。

<Carousel label="おすすめの写真" autoplay>
  <CarouselSlides>…</CarouselSlides>
  <CarouselPrevious />
  <CarouselNext />
  <CarouselMarkers />
  <CarouselPlayPause />
</Carousel>

4つのボタンは、それぞれ別の役目です

停止ボタンは、前後のボタンの代わりではありません。

役目
前へ・次へ場所を移す
止める・再生勝手に動くのを制する
印いまどこかを示す

同じ場所に置くと片方が消えます。 停止ボタンを「前へ」の位置に置くと、前へ移動する手段が無くなります。

例からどちらかが欠けたらテストが失敗するようにしてあります。

自動で動くものには止める手段が無ければなりません(WCAG 2.2.2)。 文書に書くだけでは守られないので、組み立ての側で成立しない形にしています。

黙って動き続けるほうが害が大きい——読んでいる最中に勝手に送られ、止める方法がありません。

間隔の既定は 4000ms です。選んだ値で、段から導いてはいません。autoplay={{ delay: 8000 }} のように渡せます。

触ったとき・ポインタが乗ったとき・フォーカスが入ったときは止まります。 読んでいる最中に送られるのは邪魔です。

動きを減らす設定のときは、止まった状態で始まります。 黙って動かしませんが、動かす手段は残します。

位置の見た目を自分で決めたいときは useCarouselPosition

サムネイルの列や「3 / 7」のような表示は、このフックで組めます。渡されるのは3つだけです。

selectedいま見えている枚目(0 始まり)
count何枚あるか
goTo(index)その枚へ飛ぶ
function Thumbnails({ photos }: { photos: { src: string; alt: string }[] }) {
  const { selected, count, goTo } = useCarouselPosition();
  // 1枚しかないなら出しません。押しても何も起きないものを置かないためです
  if (count <= 1) return null;
  return (
    <ul>
      {photos.map((photo, index) => (
        <li key={photo.src}>
          <button
            type="button"
            onClick={() => goTo(index)}
            aria-label={`${photo.alt}を見る`}
            aria-current={index === selected ? 'true' : undefined}
          >
            <img src={photo.src} alt="" />
          </button>
        </li>
      ))}
    </ul>
  );
}

いまどこを見ているかは、見た目だけでなく属性でも伝えてください(aria-current)。枠線の色だけで表すと、見えない人に届きません。

送りの仕組みそのものは渡していません。 渡すと、その仕組みの API がそのまま約束になり、入れ替えたときにあなたのコードが壊れます。前後のボタンと位置の印で足りるなら、CarouselPrevious / CarouselNext / CarouselMarkers を使ってください。

動きを減らす設定ではアニメーションなしで切り替わります

スクロールのアニメーションは止まり、すぐに切り替わります。利用側で何かする必要はありません。

3つの状態

通常・空・エッジケースの3つは、どのコンポーネントにも必ずあります。

通常default.tsx

通常。1枚ずつスクロールする。

一枚目の中身
二枚目の中身
三枚目の中身
四枚目の中身

空empty.tsx

空。1枚も無いとき。

エッジケースedge.tsx

エッジケース。1枚だけのときと、端で折り返すとき。

これだけ
一枚目
二枚目
三枚目
一枚目
二枚目
三枚目

props

Prop

Type

型と説明はコンポーネントのソースから抽出しています。既定値は出していません——cva の defaultVariants は 署名に現れないので、表に出すと「引数のデフォルトが書かれているものだけ」になり、嘘になります。

On this page