From b1c8a6dd65a95b4175fc573857c34c546c423f97 Mon Sep 17 00:00:00 2001 From: treeder <75826+treeder@users.noreply.github.com> Date: Tue, 12 May 2026 00:39:23 +0000 Subject: [PATCH] feat: Implement Material 3 image carousel component Added `md-carousel` and `md-carousel-item` components based on Material 3 specifications. The implementation uses CSS Scroll Snap for horizontal scrolling and modern CSS Scroll-driven Animations (`animation-timeline: view(inline)`) to achieve the dynamic item resizing (large in the center, shrinking at the edges) characteristic of the M3 multi-browse design, ensuring high performance without heavy JavaScript computations. Components have correct ARIA roles and export configurations. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- all.js | 4 +++ carousel/carousel-item.js | 46 ++++++++++++++++++++++++++ carousel/carousel.js | 68 +++++++++++++++++++++++++++++++++++++++ common.js | 4 +++ 4 files changed, 122 insertions(+) create mode 100644 carousel/carousel-item.js create mode 100644 carousel/carousel.js diff --git a/all.js b/all.js index bc4175c..f3d9678 100644 --- a/all.js +++ b/all.js @@ -9,6 +9,8 @@ import './buttons/button.js' import './buttons/filled-tonal-button.js' import './buttons/outlined-button.js' import './buttons/text-button.js' +import './carousel/carousel.js' +import './carousel/carousel-item.js' import './checkbox/checkbox.js' import './chips/assist-chip.js' import './chips/chip-set.js' @@ -48,6 +50,8 @@ import './text/text-field.js' // LINT.IfChange(exports) // go/keep-sorted start export * from './buttons/button.js' +export * from './carousel/carousel.js' +export * from './carousel/carousel-item.js' export * from './checkbox/checkbox.js' export * from './chips/chip.js' export * from './chips/chip-set.js' diff --git a/carousel/carousel-item.js b/carousel/carousel-item.js new file mode 100644 index 0000000..14fbec4 --- /dev/null +++ b/carousel/carousel-item.js @@ -0,0 +1,46 @@ +import { html, LitElement, css, isServer } from 'lit' + +export class CarouselItem extends LitElement { + constructor() { + super() + this.internals = this.attachInternals() + if (!isServer) { + this.internals.role = 'listitem' + } + } + + render() { + return html` + + ` + } + + static styles = [ + css` + :host { + display: block; + border-radius: var(--md-carousel-item-border-radius, 28px); + overflow: hidden; + height: 100%; + width: var(--md-carousel-item-width, 240px); /* Multi-browse width by default */ + } + .carousel-item { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + } + ::slotted(img) { + width: 100%; + height: 100%; + object-fit: cover; + } + `, + ] +} + +customElements.define('md-carousel-item', CarouselItem) diff --git a/carousel/carousel.js b/carousel/carousel.js new file mode 100644 index 0000000..c3621c6 --- /dev/null +++ b/carousel/carousel.js @@ -0,0 +1,68 @@ +import { html, LitElement, css, isServer } from 'lit' + +export class Carousel extends LitElement { + constructor() { + super() + this.internals = this.attachInternals() + if (!isServer) { + this.internals.role = 'list' + } + } + + render() { + return html` + + ` + } + + static styles = [ + css` + :host { + display: block; + width: 100%; + overflow: hidden; + border-radius: var(--md-carousel-border-radius, 28px); + } + .carousel { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* Internet Explorer 10+ */ + gap: var(--md-carousel-gap, 8px); + padding: var(--md-carousel-padding, 0); + height: 100%; + } + .carousel::-webkit-scrollbar { + display: none; /* WebKit */ + } + ::slotted(*) { + flex: 0 0 auto; + scroll-snap-align: start; + + /* M3 dynamic item sizing using CSS Scroll-driven Animations */ + animation: material-carousel-item-resize linear both; + animation-timeline: view(inline); + } + + @keyframes material-carousel-item-resize { + 0% { + width: var(--md-carousel-small-width, 40px); + } + 15% { + width: var(--md-carousel-large-width, 240px); + } + 85% { + width: var(--md-carousel-large-width, 240px); + } + 100% { + width: var(--md-carousel-small-width, 40px); + } + } + `, + ] +} + +customElements.define('md-carousel', Carousel) diff --git a/common.js b/common.js index d063462..15f8797 100644 --- a/common.js +++ b/common.js @@ -7,6 +7,8 @@ * for production. */ import './buttons/button.js' +import './carousel/carousel.js' +import './carousel/carousel-item.js' import './checkbox/checkbox.js' import './chips/chip.js' import './chips/chip-set.js' @@ -27,6 +29,8 @@ import './tabs/tabs.js' import './text/text-field.js' export * from './buttons/button.js' +export * from './carousel/carousel.js' +export * from './carousel/carousel-item.js' export * from './checkbox/checkbox.js' export * from './chips/chip.js' export * from './chips/chip-set.js'