Containment
Carousel
A scrolling row of items that grow and shrink as they move: multi-browse, uncontained, hero and full-screen layouts.
- COMPONENT_SPEC §36
- CSS · custom elements · React
- Open in the playground
Examples
Multi-browse
Large, medium and small items sized from Compose's keylines; scroll it and the items grow and shrink, staying pinned together.
The live demo runs with JavaScript. Its code is below.
import { Carousel, CarouselItem, Icon } from '@kyralabs/m3exp-web/react';
const ITEMS = [
{ icon: 'landscape', label: 'Mountains', color: 'primary' },
{ icon: 'forest', label: 'Forest', color: 'secondary' },
{ icon: 'beach_access', label: 'Beach', color: 'tertiary' },
{ icon: 'sailing', label: 'Sailing', color: 'primary' },
{ icon: 'hiking', label: 'Hiking', color: 'secondary' },
{ icon: 'kayaking', label: 'Kayaking', color: 'tertiary' },
];
/** About 200px, from spacing tokens. */
const TILE_HEIGHT =
'calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800))';
export default function MultiBrowse() {
return (
<Carousel aria-label="Trips" itemWidth={186} itemSpacing={8} style={{ inlineSize: '100%' }}>
{ITEMS.map((item, i) => (
<CarouselItem
key={i}
onPress={() => {
console.log(i);
}}
>
<div
style={{
display: 'grid',
placeItems: 'center',
alignContent: 'center',
blockSize: TILE_HEIGHT,
backgroundColor: `var(--m3e-sys-color-${item.color}-container)`,
color: `var(--m3e-sys-color-on-${item.color}-container)`,
}}
>
<Icon name={item.icon} />
<span>{item.label}</span>
</div>
</CarouselItem>
))}
</Carousel>
);
}
<m3e-carousel
label="Trips"
item-width="186"
item-spacing="8"
style="display: block; inline-size: 100%"
>
<m3e-carousel-item action>
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-primary-container);
color: var(--m3e-sys-color-on-primary-container);
"
>
<span class="m3e-icon" aria-hidden="true">landscape</span>
<span>Mountains</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item action>
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-secondary-container);
color: var(--m3e-sys-color-on-secondary-container);
"
>
<span class="m3e-icon" aria-hidden="true">forest</span>
<span>Forest</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item action>
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-tertiary-container);
color: var(--m3e-sys-color-on-tertiary-container);
"
>
<span class="m3e-icon" aria-hidden="true">beach_access</span>
<span>Beach</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item action>
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-primary-container);
color: var(--m3e-sys-color-on-primary-container);
"
>
<span class="m3e-icon" aria-hidden="true">sailing</span>
<span>Sailing</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item action>
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-secondary-container);
color: var(--m3e-sys-color-on-secondary-container);
"
>
<span class="m3e-icon" aria-hidden="true">hiking</span>
<span>Hiking</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item action>
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-tertiary-container);
color: var(--m3e-sys-color-on-tertiary-container);
"
>
<span class="m3e-icon" aria-hidden="true">kayaking</span>
<span>Kayaking</span>
</div>
</m3e-carousel-item>
</m3e-carousel>
Run the HTML version (custom elements, no framework).
Uncontained
Items keep their width and the last visible one is cut off at the edge. It scrolls freely, without snapping.
The live demo runs with JavaScript. Its code is below.
import { Carousel, CarouselItem, Icon } from '@kyralabs/m3exp-web/react';
const ITEMS = [
{ icon: 'landscape', label: 'Mountains', color: 'primary' },
{ icon: 'forest', label: 'Forest', color: 'secondary' },
{ icon: 'beach_access', label: 'Beach', color: 'tertiary' },
{ icon: 'sailing', label: 'Sailing', color: 'primary' },
{ icon: 'hiking', label: 'Hiking', color: 'secondary' },
{ icon: 'kayaking', label: 'Kayaking', color: 'tertiary' },
];
/** About 200px, from spacing tokens. */
const TILE_HEIGHT =
'calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800))';
export default function Uncontained() {
return (
<Carousel
aria-label="Recent trips"
layout="uncontained"
itemWidth={186}
itemSpacing={8}
style={{ inlineSize: '100%' }}
>
{ITEMS.map((item) => (
<CarouselItem key={item.label}>
<div
style={{
display: 'grid',
placeItems: 'center',
alignContent: 'center',
blockSize: TILE_HEIGHT,
backgroundColor: `var(--m3e-sys-color-${item.color}-container)`,
color: `var(--m3e-sys-color-on-${item.color}-container)`,
}}
>
<Icon name={item.icon} />
<span>{item.label}</span>
</div>
</CarouselItem>
))}
</Carousel>
);
}
<m3e-carousel label="Recent trips" layout="uncontained" item-width="186" item-spacing="8">
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-primary-container);
color: var(--m3e-sys-color-on-primary-container);
"
>
<span class="m3e-icon" aria-hidden="true">landscape</span>
<span>Mountains</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-secondary-container);
color: var(--m3e-sys-color-on-secondary-container);
"
>
<span class="m3e-icon" aria-hidden="true">forest</span>
<span>Forest</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-tertiary-container);
color: var(--m3e-sys-color-on-tertiary-container);
"
>
<span class="m3e-icon" aria-hidden="true">beach_access</span>
<span>Beach</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-primary-container);
color: var(--m3e-sys-color-on-primary-container);
"
>
<span class="m3e-icon" aria-hidden="true">sailing</span>
<span>Sailing</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-secondary-container);
color: var(--m3e-sys-color-on-secondary-container);
"
>
<span class="m3e-icon" aria-hidden="true">hiking</span>
<span>Hiking</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-tertiary-container);
color: var(--m3e-sys-color-on-tertiary-container);
"
>
<span class="m3e-icon" aria-hidden="true">kayaking</span>
<span>Kayaking</span>
</div>
</m3e-carousel-item>
</m3e-carousel>
Run the HTML version (custom elements, no framework).
Hero
One large item and a small one after it, start-aligned, one item per swipe. Without itemWidth the large item fills the width but for the small one.
The live demo runs with JavaScript. Its code is below.
import { Carousel, CarouselItem, Icon } from '@kyralabs/m3exp-web/react';
const ITEMS = [
{ icon: 'landscape', label: 'Mountains', color: 'primary' },
{ icon: 'forest', label: 'Forest', color: 'secondary' },
{ icon: 'beach_access', label: 'Beach', color: 'tertiary' },
{ icon: 'sailing', label: 'Sailing', color: 'primary' },
{ icon: 'hiking', label: 'Hiking', color: 'secondary' },
{ icon: 'kayaking', label: 'Kayaking', color: 'tertiary' },
];
/** About 200px, from spacing tokens. */
const TILE_HEIGHT =
'calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800))';
export default function Hero() {
return (
<Carousel
aria-label="Featured trips"
layout="hero"
itemSpacing={8}
style={{ inlineSize: '100%' }}
>
{ITEMS.map((item) => (
<CarouselItem key={item.label}>
<div
style={{
display: 'grid',
placeItems: 'center',
alignContent: 'center',
blockSize: TILE_HEIGHT,
backgroundColor: `var(--m3e-sys-color-${item.color}-container)`,
color: `var(--m3e-sys-color-on-${item.color}-container)`,
}}
>
<Icon name={item.icon} />
<span>{item.label}</span>
</div>
</CarouselItem>
))}
</Carousel>
);
}
<m3e-carousel label="Featured trips" layout="hero" item-spacing="8">
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-primary-container);
color: var(--m3e-sys-color-on-primary-container);
"
>
<span class="m3e-icon" aria-hidden="true">landscape</span>
<span>Mountains</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-secondary-container);
color: var(--m3e-sys-color-on-secondary-container);
"
>
<span class="m3e-icon" aria-hidden="true">forest</span>
<span>Forest</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-tertiary-container);
color: var(--m3e-sys-color-on-tertiary-container);
"
>
<span class="m3e-icon" aria-hidden="true">beach_access</span>
<span>Beach</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-primary-container);
color: var(--m3e-sys-color-on-primary-container);
"
>
<span class="m3e-icon" aria-hidden="true">sailing</span>
<span>Sailing</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-secondary-container);
color: var(--m3e-sys-color-on-secondary-container);
"
>
<span class="m3e-icon" aria-hidden="true">hiking</span>
<span>Hiking</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-tertiary-container);
color: var(--m3e-sys-color-on-tertiary-container);
"
>
<span class="m3e-icon" aria-hidden="true">kayaking</span>
<span>Kayaking</span>
</div>
</m3e-carousel-item>
</m3e-carousel>
Run the HTML version (custom elements, no framework).
Centered hero
One large item centered between two small ones; the first and last items align with the edges instead.
The live demo runs with JavaScript. Its code is below.
import { Carousel, CarouselItem, Icon } from '@kyralabs/m3exp-web/react';
const ICONS = ['photo_camera', 'palette', 'music_note', 'movie', 'menu_book'];
/** About 200px, from spacing tokens. */
const TILE_HEIGHT =
'calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800))';
export default function CenteredHero() {
return (
<Carousel
aria-label="Collections"
layout="centered-hero"
itemSpacing={8}
style={{ inlineSize: '100%' }}
>
{ICONS.map((icon) => (
<CarouselItem key={icon} aria-label={icon.replace('_', ' ')}>
<div
style={{
display: 'grid',
placeItems: 'center',
blockSize: TILE_HEIGHT,
backgroundColor: 'var(--m3e-sys-color-surface-container-highest)',
}}
>
<Icon name={icon} />
</div>
</CarouselItem>
))}
</Carousel>
);
}
<m3e-carousel
label="Collections"
layout="centered-hero"
item-spacing="8"
style="display: block; inline-size: 100%"
>
<m3e-carousel-item label="photo camera">
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-surface-container-highest);
"
>
<span class="m3e-icon" aria-hidden="true">photo_camera</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item label="palette">
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-surface-container-highest);
"
>
<span class="m3e-icon" aria-hidden="true">palette</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item label="music note">
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-surface-container-highest);
"
>
<span class="m3e-icon" aria-hidden="true">music_note</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item label="movie">
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-surface-container-highest);
"
>
<span class="m3e-icon" aria-hidden="true">movie</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item label="menu book">
<div
style="
display: grid;
place-items: center;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800)
);
background-color: var(--m3e-sys-color-surface-container-highest);
"
>
<span class="m3e-icon" aria-hidden="true">menu_book</span>
</div>
</m3e-carousel-item>
</m3e-carousel>
Run the HTML version (custom elements, no framework).
Full screen
One item fills the carousel and the items scroll vertically, snapping to each (m3.material.io; Compose has no full-screen carousel). Give the carousel a height; ArrowUp and ArrowDown move between items.
The live demo runs with JavaScript. Its code is below.
import { Carousel, CarouselItem, Icon } from '@kyralabs/m3exp-web/react';
const ITEMS = [
{ icon: 'landscape', label: 'Mountains', color: 'primary' },
{ icon: 'forest', label: 'Forest', color: 'secondary' },
{ icon: 'beach_access', label: 'Beach', color: 'tertiary' },
{ icon: 'sailing', label: 'Sailing', color: 'primary' },
];
/** The carousel needs a height: about 420px, from spacing tokens. */
const HEIGHT =
'calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-800) + var(--m3e-sys-space-800))';
export default function FullScreen() {
return (
<Carousel
aria-label="Stories"
layout="full-screen"
style={{ inlineSize: '100%', blockSize: HEIGHT }}
>
{ITEMS.map((item) => (
<CarouselItem key={item.label}>
<div
style={{
display: 'grid',
placeItems: 'center',
alignContent: 'center',
blockSize: '100%',
backgroundColor: `var(--m3e-sys-color-${item.color}-container)`,
color: `var(--m3e-sys-color-on-${item.color}-container)`,
}}
>
<Icon name={item.icon} />
<span>{item.label}</span>
</div>
</CarouselItem>
))}
</Carousel>
);
}
<!-- The host is display: contents; a grid stretches the carousel to the height. -->
<div
style="
display: grid;
inline-size: 100%;
block-size: calc(
var(--m3e-sys-space-900) + var(--m3e-sys-space-900) + var(--m3e-sys-space-900) +
var(--m3e-sys-space-900) + var(--m3e-sys-space-800) + var(--m3e-sys-space-800)
);
"
>
<m3e-carousel label="Stories" layout="full-screen">
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: 100%;
background-color: var(--m3e-sys-color-primary-container);
color: var(--m3e-sys-color-on-primary-container);
"
>
<span class="m3e-icon" aria-hidden="true">landscape</span>
<span>Mountains</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: 100%;
background-color: var(--m3e-sys-color-secondary-container);
color: var(--m3e-sys-color-on-secondary-container);
"
>
<span class="m3e-icon" aria-hidden="true">forest</span>
<span>Forest</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: 100%;
background-color: var(--m3e-sys-color-tertiary-container);
color: var(--m3e-sys-color-on-tertiary-container);
"
>
<span class="m3e-icon" aria-hidden="true">beach_access</span>
<span>Beach</span>
</div>
</m3e-carousel-item>
<m3e-carousel-item>
<div
style="
display: grid;
place-items: center;
align-content: center;
block-size: 100%;
background-color: var(--m3e-sys-color-primary-container);
color: var(--m3e-sys-color-on-primary-container);
"
>
<span class="m3e-icon" aria-hidden="true">sailing</span>
<span>Sailing</span>
</div>
</m3e-carousel-item>
</m3e-carousel>
</div>
Run the HTML version (custom elements, no framework).
Usage
A horizontally scrolling row of visual items whose visible size changes as they scroll (large, medium, small), pinned together; one item per screen for full screen.
API
React
<Carousel>
Carousel (COMPONENT_SPEC §36; Compose HorizontalMultiBrowseCarousel / HorizontalUncontainedCarousel / HorizontalCenteredHeroCarousel, owner decisions M9 #22–#26): a WAI-ARIA APG carousel (role="region", aria-roledescription="carousel", named by aria-label) of <CarouselItem> slides. m3e-elements' CarouselLayout (the code <m3e-carousel> runs) sizes and masks the items from Compose's keylines as they scroll, snaps them natively, moves between them with the arrow keys and scrolls a focused item to its snap position. Other props go to the .m3e-carousel root; the ref is a CarouselHandle.
| Prop | Type | Default | Description |
|---|---|---|---|
layout
|
"multi-browse" | "uncontained" | "hero" | "centered-hero" | "full-screen"
|
multi-browse |
Default multi-browse.
|
itemWidth
|
number | undefined
|
px. Multi-browse and hero: preferredItemWidth (hero without it fills the carousel but for
the small items); uncontained: itemWidth. Multi-browse and uncontained need it.
|
|
itemSpacing
|
number | undefined
|
px between items (default CarouselDefaults.ItemSpacing, 0dp). | |
contentPadding
|
number | undefined
|
px before the first and after the last item (default CarouselDefaults.ContentPadding, 0dp). | |
itemLabel
|
((index: number, count: number) => string) | undefined
|
Item names (default "n of N"). | |
children
|
ReactNode
|
<CarouselItem>s.
|
Also accepts HTML attributes not listed here.
<CarouselItem>
One item of a Carousel. Renders nothing itself: <Carousel> renders the .m3e-carousel__item contract around its children.
| Prop | Type | Default | Description |
|---|---|---|---|
href
|
string | undefined
|
Makes the item a link (<a>): state layer and ripple (owner decision M9 #25).
|
|
target
|
string | undefined
|
The link's target. | |
aria-label
|
string | undefined
|
The item's accessible name (default: "2 of 5", or itemLabel).
|
|
children
|
ReactNode
|
||
onPress
|
((e: PressEvent) => void) | undefined
|
Makes the item a button (unless href): state layer and ripple.
|
Custom elements
<m3e-carousel>
import '@kyralabs/m3e-elements/carousel.js';
<m3e-carousel layout="multi-browse" item-width="186" label="Featured"> (COMPONENT_SPEC §36; Compose HorizontalMultiBrowseCarousel / HorizontalUncontainedCarousel / HorizontalCenteredHeroCarousel, owner decisions M9 #22–#26). Renders the m3e-css .m3e-carousel contract inside itself (the host is display: contents) from its <m3e-carousel-item> children, the DOM m3e-react's <Carousel> renders, and runs CarouselLayout (shared with m3e-react; the carousel-layout entry, imported when the element first renders, ready): Compose's keylines, masks and snap positions, the arrow keys and the scroll to a focused item.
layout:multi-browse(default),uncontained,hero(start-aligned),centered-heroorfull-screen(CSS only: one item filling the carousel, scrolling vertically).item-width(px): multi-browse and heropreferredItemWidth(hero without it fills the carousel but for the small items), uncontaineditemWidth. Multi-browse and uncontained need it.item-spacing,content-padding(px; default CarouselDefaults' 0dp).label: the carousel's accessible name (APG carousel:role="region",aria-roledescription="carousel"); items arerole="group"slides named "n of N".scrollToItem(index),currentItem. Eventaction(detail: { index }), dispatched by anactionitem and bubbling through the carousel;item-click(same detail, on the carousel) is deprecated since 1.8 and removed in 2.0.
| Attribute | Property | Type | Notes |
|---|---|---|---|
layout
|
layout
|
string
|
Reflected. |
item-width
|
itemWidth
|
number
|
|
item-spacing
|
itemSpacing
|
number
|
|
content-padding
|
contentPadding
|
number
|
|
label
|
label
|
string
|
| Event | Detail | Description |
|---|---|---|
action
|
CarouselActionDetail
|
An action item was activated (from the <m3e-carousel-item>, bubbling).
|
item-click
|
CarouselActionDetail
|
Deprecated.
Since 1.8: listen to action. Removed in 2.0.
|
<m3e-carousel-item>
import '@kyralabs/m3e-elements/carousel.js';
<m3e-carousel-item>…</m3e-carousel-item> — one item of a <m3e-carousel>: its child nodes move into the rendered item's mask. href makes the item a link, action a button (activating it dispatches action, detail: { index }, from the item; it bubbles); both draw the state layer and ripple (owner decision M9 #25). label replaces the default name ("2 of 5").
| Attribute | Property | Type | Notes |
|---|---|---|---|
href
|
href
|
string
|
|
target
|
target
|
string
|
|
action
|
action
|
boolean
|
|
label
|
label
|
string
|
| Event | Detail | Description |
|---|---|---|
action
|
CarouselActionDetail
|
An action item was activated (it bubbles to the carousel).
|
CSS
carousel.css
@import '@kyralabs/m3e-css/carousel.css';
Carousel (COMPONENT_SPEC §36; Compose HorizontalMultiBrowseCarousel / HorizontalUncontainedCarousel / HorizontalCenteredHeroCarousel, owner decisions M9 #22–#26): a horizontal scroll container of items with native scroll snapping. Without JavaScript the items keep their full width (--m3e-comp-carousel-item-width) and snap to the start; the m3e-elements carousel layout (<m3e-carousel>, m3e-react's <Carousel>) sizes them from Compose's keylines and masks them as they scroll.
<div class="m3e-carousel m3e-carousel--multi-browse" role="region"
aria-roledescription="carousel" aria-label="Featured" style="--m3e-comp-carousel-item-width: 186px">
<div class="m3e-carousel__viewport" tabindex="-1">
<div class="m3e-carousel__item" role="group" aria-roledescription="slide"
aria-label="1 of 5" tabindex="0">
<div class="m3e-carousel__mask"><img src="…" alt="…"></div>
</div>
<div class="m3e-carousel__item" role="group" aria-roledescription="slide"
aria-label="2 of 5">
<a class="m3e-carousel__mask m3e-carousel__action" href="…">…</a> (interactive)
</div>
</div>
</div>
- Layouts: --multi-browse, --uncontained, --hero (start-aligned), --centered-hero (Compose's
keyline math, m3e-motion carousel.ts) and --full-screen (CSS only, owner decision M9 #22: one
item filling the container, scrolling vertically, 16dp apart, no padding or corners; D19 (20),
SOURCES register #83).
- Snapping (owner decision M9 #23): every item snaps, one at a time (scroll-snap-stop), to its
keyline (Compose's KeylineSnapPosition, a scroll margin the layout writes); the uncontained
layout does not snap (noSnapFlingBehavior). The fling is the browser's.
- Items: the mask is the item's visible part, rounded extra-large (the samples'
maskClip(MaterialTheme.shapes.extraLarge); owner decision M9 #25), clipping its content. An
interactive item's mask is its link or button (.m3e-carousel__action): state layer and ripple
over the content, no elevation. Static items take the focus themselves (tabindex="0"); every
item is a tab stop and the layout's arrow keys move between them (owner decision M9 #26).
- Reduced motion (owner decision M9 #24, kept by D19 (21), register #84; m3.material.io): items
keep one size (the focal one) and are not masked; the layout stops writing the keyline
variables.
- Spacing and padding: --m3e-comp-carousel-item-spacing and
--m3e-comp-carousel-content-padding-start / -end (CarouselDefaults: 0dp, D19 (19), register
#82; the samples and m3.material.io use 8 / 16dp). Set them on the carousel.
- JS hooks (the layout): [data-keylines] while it masks; --_m3e-carousel-item-size on the root;
per item --_m3e-carousel-snap (its scroll margin), --_m3e-carousel-inset (the mask's inset on
each side), --_m3e-carousel-translate (physical) and --_m3e-carousel-z; [data-scrolling] while
it scrolls on its spring (snapping is off meanwhile).
7 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-carousel-content-padding-end
|
0px |
--m3e-comp-carousel-content-padding-start
|
0px |
--m3e-comp-carousel-full-screen-item-spacing
|
16px |
--m3e-comp-carousel-item-shape
|
var(--m3e-sys-shape-corner-extra-large) |
--m3e-comp-carousel-item-spacing
|
0px |
--m3e-comp-carousel-item-state-layer-color
|
var(--m3e-sys-color-on-surface) |
--m3e-comp-carousel-item-width
|
set by the component |
Differences from Compose
browser fling physics instead of Compose's (one-item fling and snap spring k400; owner decision M9 #23); no mouse drag-to-scroll (touch, trackpads, Shift + wheel and the keyboard scroll it); CarouselItemDrawInfo (the item's current size) is not exposed to content; the uncontained items are itemWidth + itemSpacing wide, as Compose lays them out (uncontainedKeylineList L168); m3.material.io's Up / Down to leave the carousel is not built in (every item is a Tab stop); RTL is read when the layout runs (a later direction change needs a re-render).
Sources
Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from
m3.material.io, as cited below from COMPONENT_SPEC §36 and the m3e-css sources.
carousel/Carousel.kt, Arrangement.kt, Keylines.kt, KeylineList.kt, Strategy.kt, KeylineSnapPosition.kt, CarouselState.kt, CarouselItemScope.kt, CarouselSamples.kt and the androidHostTest suites (vendored, androidx-material3-carousel); animation-core AnimationSpec.kt, VectorizedAnimationSpec.kt; m3.material.io Carousel (page b0726223-2639-4157-84ae-cb84c07ee2e5: Specs, Guidelines, Accessibility; token DB TOKEN_TABLE.43a124d2adc8a2ef, md.comp.carousel-item).
carousel.css sources
(values: carousel-defaults.css, lines in PARITY.md): compose/material3 @ 19991f2c carousel/Carousel.kt (CarouselDefaults, Modifier.carouselItem), CarouselSamples.kt; m3.material.io Carousel (Specs, Accessibility; token DB md.comp.carousel-item).