本文へスキップ
Material 3 Expressivev1.3.0-rc.1
コンテナー · 準拠済み

Carousel

CarouselCarouselItemCarouselLayoutCarouselPropsCarouselScrollMultiAspectCarouselItem
playground/examples/Carousel.example.tsx

Carouselは、コンテナー内を移動するとサイズが変わる、主に視覚的な項目のスクロール可能な一覧を表示します。Materialが定める6種類のレイアウトを1つのコンポーネントで扱います。

import { Button, Carousel } from '@language-lit/material3-expressive'
import '@language-lit/material3-expressive/styles.css'

// Multi-browse: many items at once, for quick browsing.
<Carousel
  aria-label="Recent photos"
  preferredItemWidth={186}
  items={photos.map((photo) => ({
    key: photo.id,
    label: photo.title,
    content: <img src={photo.src} alt="" />,
    onActivate: () => open(photo),
  }))}
/>

// Hero: one large item with a preview of what is next.
<Carousel aria-label="Featured" layout="centeredHero" items={featured} />

// Uncontained: same-size items that flow past the edge.
<Carousel aria-label="Articles" layout="uncontained" itemWidth={240} items={articles} />

// Multi-aspect ratio: each item keeps its own shape.
<Carousel
  aria-label="Clips"
  layout="multiAspect"
  items={clips.map((clip) => ({ key: clip.id, content: <video src={clip.src} />, aspectRatio: clip.ratio }))}
/>

仕様

項目一覧はitemsで渡します。各項目にはkeyと表示用のcontentを指定し、任意でlabel、onActivate、href、disabledを指定できます。onActivateがある項目は実際のbutton、hrefがある項目は実際のaとして描画します。Space/Enterによるアクティベーションと、フォーカス時にブラウザーが項目をスクロール領域へ表示する動作は、キー処理を独自実装せずプラットフォームに任せます。

layoutで配置方法を選択します。既定値は"multiBrowse"です。

レイアウト用途必須のprop
multiBrowse多数の視覚的な項目を一度に見るpreferredItemWidth
uncontainedテキスト主体、または高度にカスタマイズした項目itemWidth
multiAspect形状が実際に異なる項目各項目のaspectRatio
hero非常に大きな項目を目立たせる—
centeredHero2つのプレビューの中央に同じ項目を配置する—
fullScreen没入型の縦方向フィード—

preferredItemWidthは目標幅であり、保証値ではありません。配置処理はまず小項目、次に中項目を調整し、最後に大項目の幅を調整して、整数個の項目がコンテナーに収まるようにします。minSmallItemWidthとmaxSmallItemWidthで小項目の幅を制限します。既定範囲は仕様に基づく40〜56pxです。

これはレイアウトで使える唯一のレスポンシブ調整手段で、値は利用側が変更します。コンテナーが広がると項目数は増えます。たとえば幅を186に固定すると、狭い幅では3項目、1440pxでは8項目が収まりますが、項目そのものは大きくなりません。Materialのガイドラインでは、ウィンドウ幅600dp未満をcompactとし、その幅では最大3項目を想定します。また、ウィンドウが広がると項目数だけでなく項目も大きくすることを求めています。このアルゴリズムで自動的に得られるのは前者だけなので、後者も必要なら広いブレークポイントでpreferredItemWidthを増やしてください。このpropは数値なので、ウィンドウサイズクラス用のフックやResizeObserverで変更できます。参照すべき規定の拡大率はありません。ファーストパーティのComposeサンプルはすべてのウィンドウ幅で186dpを固定使用しているため、ここでの例も同じ値にしています。

配置に大項目が複数入る場合に起きる動作を知っておくと、問題と誤解せずに済みます。大項目のキ―ラインが複数あるとサイズが同じになるため、項目はサイズを変えずにその間を移動し、末尾側の端で初めてフレームに収まります。これは仕様どおりです。ブラウズ領域は安定し、端でサイズが変わるため、幅の広いmulti-browseは狭い場合と異なるアニメーションになります。

scrollは仕様に記載された2つの動作から選択し、既定ではレイアウトに推奨されるものを使います。2種類のuncontained以外は"snap"です。uncontainedの2種類は"free"になります。full-screenレイアウトではスナップが必須なので、変更しないでください。

currentItem、defaultCurrentItem、onCurrentItemChangeで焦点位置にある項目を管理します。currentItemを設定するとその項目までスクロールし、スクロールすると最寄りの項目が通知されます。itemSpacingで間隔を上書きできます。

Carouselにはアクセシブルな名前が必要なので、aria-labelまたはaria-labelledbyを指定します。divのほかの属性はそのまま渡され、refはスクロールコンテナーを参照します。

項目の適応型コンテンツ

項目の現在幅が通知されるため、再レンダーせずにコンテンツを適応させられます。各項目のdata-m3e-sizeはlarge、medium、smallのいずれかです。data-m3e-carousel-hideを付けたコンテンツは、項目幅がそのコンテンツに必要な幅より狭くなるにつれてフェードアウトします。

<Carousel
  aria-label="Albums"
  preferredItemWidth={186}
  items={albums.map((album) => ({
    key: album.id,
    label: album.title,
    content: (
      <>
        <img src={album.cover} alt="" />
        <figcaption>
          <span data-m3e-carousel-hide="medium">{album.title}</span>
          <span data-m3e-carousel-hide="small">{album.year}</span>
        </figcaption>
      </>
    ),
  }))}
/>

これはMaterial独自のルールです。大項目にはタイトル全体、中項目ではタイトルを隠し、小項目ではラベルを短縮表示します。参照実装にならって、単純に表示を切り替えるのではなくフェードさせます。タイトルはマスクの端に沿って配置され、項目の幅が不足してくると薄くなります。

属性値は、コンテンツが残る時間を指定するものであり、表示/非表示の境界ではありません。"medium"のコンテンツは焦点位置では不透明で、サイズ範囲の中間までに消えます。"small"は中間位置までに不透明になり、最も狭い表示項目では消えます。そのためタイトルから先に薄くなり、各変化を切り替えではなくフェードとして確認できます。

フェード中のテキストを切り抜かずに定位置へ保つには、参照実装と同様に--m3e-carousel-item-inset-startで移動します。余白の追加より移動を使ってください。余白はコンテンツ自体の幅を変えます。配置アルゴリズムが処理できる項目幅を超えるコンテンツは配置できません。

正確なピクセル幅は--m3e-carousel-item-current-size、--m3e-carousel-item-min-size、--m3e-carousel-item-max-sizeからも取得できます。

これらの値を直接使う場合は注意してください。--m3e-carousel-item-min-sizeはComposeの値をそのまま表し、画面外に少しだけ見える、約10pxのアンカーキ―ラインも含みます。表示される項目の最小幅ではないため、これを使ってサイズを正規化すると、ほとんどすべての項目がmediumに分類されます。サイズ判定には小キ―ラインを使うdata-m3e-sizeを推奨します。

コンテンツは項目に合わせて調整されず、項目によってクリップされます。スナップ位置は項目幅から導かれるため、項目の幅は配置時の幅そのものです。幅が狭くなっても読みやすさを保つ必要があるテキストには、マスクのインセットである--m3e-carousel-item-inset-startと--m3e-carousel-item-inset-endを適用します。そうしない場合、テキストは移動せず、字形の途中で切り取られます。

すべて表示 — アクセシビリティ要件

縦方向にスクロールするページ上の横方向Carouselでは、横スクロール以外の方法でも全項目へ移動できるようにします。MaterialはCarouselの下にすべて表示ボタンを置くか、見出しの横に矢印を置くよう求めています。これはpropではなく、次のように組み合わせます。

<section aria-labelledby="recent-heading">
  <h2 id="recent-heading">Recent</h2>
  <Carousel aria-labelledby="recent-heading" preferredItemWidth={186} items={photos} />
  <Button variant="text" onClick={() => router.push('/photos')}>
    Show all
  </Button>
</section>

full-screenレイアウトではページと同じ軸にスクロールするため、この要件は適用されません。

動作

Carouselは実際のスクロールコンテナーです。ジェスチャー、ホイール、慣性スクロール、キーボードスクロール、RTLへの対応はブラウザーが担います。スナップにはMaterial独自のキ―ラインに合わせたCSS Scroll Snapを使うため、ジェスチャーを離すと配置仕様に従った位置で停止します。フリング距離はブラウザーが決めます。Materialの「1回のフリングで1項目だけ進む」動作に、Web上の相当機能はありません。

項目のマスキングはスクロール位置から計算します。これにより項目が大、中、小の幅に変化し、マスク内の画像に視差効果が生じます。

prefers-reduced-motion: reduceではマスキングを解除します。各項目は最大幅のままとなり、拡大縮小せず、コンテナー端まで届きます。これはMaterialが定めるモーションを減らした場合の表示です。

トークン

Carouselは、トークンが見た目だけでなくレイアウトも変える唯一のコンポーネントです。4つのトークンは配置アルゴリズムに直接使われます。

トークン既定値効果
--m3e-comp-carousel-min-small-item-size40px小項目の最小幅
--m3e-comp-carousel-max-small-item-size56px小項目の最大幅
--m3e-comp-carousel-anchor-size10px項目が両端を越えて移動する距離
--m3e-comp-carousel-medium-large-item-diff-threshold0.85medium項目がlarge項目に近づきすぎたと判断する境界

そのほかは通常の見た目に関するトークンです。container-color、item-shape、item-container-color、item-content-color、item-spacing、leading-padding、trailing-padding、block-padding、uncontained-trailing-padding、full-screen-padding、full-screen-item-spacing、フォーカスリングの3値、state-layerの色、無効状態の不透明度2種類です。テーマまたはインスタンス単位で上書きできます。

<Carousel
  aria-label="Covers"
  preferredItemWidth={186}
  items={covers}
  style={{ '--m3e-comp-carousel-item-shape': '12px' }}
/>

レイアウト上の注意

Carouselには高さを指定してください。項目はコンテナーいっぱいに広がり、multi-aspectレイアウトでは高さと項目の比率から幅を求めます。

項目のコンテンツはマスク前の項目サイズでレイアウトされ、クリップされます。画像は項目いっぱいに広げてください。子要素がimg、video、picture、svg、canvasの場合は適切なサイズと切り抜きを適用します。