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) |
slidesToScroll | 1回で何枚スクロールするか |
dragFree | 吸着せずに慣性で流す |
startIndex | 何枚目から始めるか |
全部の一覧は Embla の設定にあります。
duration だけ渡せません。 スクロールの速さは動きを減らす設定(prefers-reduced-motion)から決めているので、
渡せるようにすると利用側がその設定を打ち消せてしまいます。
尊重するのは利用側の責務ではありません。
前後のボタンと位置の表示は Carousel が描きます
ブラウザが作ってくれる仕組み(::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 は 署名に現れないので、表に出すと「引数のデフォルトが書かれているものだけ」になり、嘘になります。