Skip to content
ShineUI
Install

Galleria

Hero plus thumbnails.

<sui-galleria>MediaStable

About

Hero image with a thumbnail strip, for multi-page receipts and document viewers. The active thumb wears an orange ring and the caption fades up over the hero. Arrow keys, a swipe and the previous and next navigators step through the images, the strip scrolls once the thumbs outrun the card, and full-screen opens the viewer over the page.

Usage

<sui-galleria height="200"></sui-galleria> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-galleria"); el.items = [ { src: "/docs/assets/imagery/manufacturing.webp", alt: "A forklift on a warehouse floor beside stacked pallets", caption: "Manufacturing clients" }, { src: "/docs/assets/imagery/construction.webp", alt: "A hard hat resting on rolled plans, with workers in hi-vis vests behind", caption: "Construction clients", }, { src: "/docs/assets/imagery/nonprofits.webp", alt: "Two volunteers in blue shirts filling paper grocery bags", caption: "Nonprofit clients" }, { src: "/docs/assets/imagery/real-estate.webp", alt: "An aerial view of a suburban street lined with houses and lawns", caption: "Real estate clients" }, ]; /* SuiGalleriaItem[] */ el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiGalleria } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { src: "/docs/assets/imagery/manufacturing.webp", alt: "A forklift on a warehouse floor beside stacked pallets", caption: "Manufacturing clients" }, { src: "/docs/assets/imagery/construction.webp", alt: "A hard hat resting on rolled plans, with workers in hi-vis vests behind", caption: "Construction clients", }, { src: "/docs/assets/imagery/nonprofits.webp", alt: "Two volunteers in blue shirts filling paper grocery bags", caption: "Nonprofit clients" }, { src: "/docs/assets/imagery/real-estate.webp", alt: "An aerial view of a suburban street lined with houses and lawns", caption: "Real estate clients" }, ]; /* SuiGalleriaItem[] */ return <SuiGalleria height="200" items={items} onSuiClose={(e) => console.log(e.detail)} />; }
<script setup> import { SuiGalleria } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { src: "/docs/assets/imagery/manufacturing.webp", alt: "A forklift on a warehouse floor beside stacked pallets", caption: "Manufacturing clients" }, { src: "/docs/assets/imagery/construction.webp", alt: "A hard hat resting on rolled plans, with workers in hi-vis vests behind", caption: "Construction clients", }, { src: "/docs/assets/imagery/nonprofits.webp", alt: "Two volunteers in blue shirts filling paper grocery bags", caption: "Nonprofit clients" }, { src: "/docs/assets/imagery/real-estate.webp", alt: "An aerial view of a suburban street lined with houses and lawns", caption: "Real estate clients" }, ]; // SuiGalleriaItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiGalleria height="200" :items="items" @sui-close="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiGalleria } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiGalleria], template: `<sui-galleria height="200" [(open)]="open" [items]="items" (suiClose)="onChange($event)"></sui-galleria>`, }) export class Example { open = false; items = [ { src: "/docs/assets/imagery/manufacturing.webp", alt: "A forklift on a warehouse floor beside stacked pallets", caption: "Manufacturing clients" }, { src: "/docs/assets/imagery/construction.webp", alt: "A hard hat resting on rolled plans, with workers in hi-vis vests behind", caption: "Construction clients", }, { src: "/docs/assets/imagery/nonprofits.webp", alt: "Two volunteers in blue shirts filling paper grocery bags", caption: "Nonprofit clients" }, { src: "/docs/assets/imagery/real-estate.webp", alt: "An aerial view of a suburban street lined with houses and lawns", caption: "Real estate clients" }, ]; // SuiGalleriaItem[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

items is a property: src, an optional thumbSrc, alt and caption per image. The caption fades up over the hero; the active thumb wears the orange ring and aria-current.

<div style="max-width:460px"><sui-galleria height="200"></sui-galleria></div>
// setup(root): properties and events setup(root) { root.querySelector('sui-galleria').items = IMAGES; }

Many pages

The strip scrolls once the thumbs outrun the card and brings the active thumb into view. Set thumbSrc on each item so a twelve-page gallery does not download every full-size scan twice.

<div style="max-width:460px"><sui-galleria id="gal-many" height="200"></sui-galleria></div>
// setup(root): properties and events setup(root) { root.querySelector('#gal-many').items = pages(); }

Navigators

Previous and next sit over the hero by default. They disable at the ends and leave focus where it was; the arrow keys still carry focus to the thumb. hide-arrows takes them off when the strip is enough, as here.

<div style="max-width:460px"><sui-galleria id="gal-nav" height="200" hide-arrows></sui-galleria></div>
// setup(root): properties and events setup(root) { root.querySelector('#gal-nav').items = IMAGES; }

Autoplay and circular

autoplay is the interval in milliseconds; circular wraps past either end. A pause control sits at the top start of the hero; a second press starts it again. The viewer waits on hover, on focus inside it and on a hidden tab, stops for good on the first thumb, navigator, key or swipe, and never runs under reduced motion.

<div style="max-width:460px"><sui-galleria id="gal-auto" height="200" circular autoplay="3000"></sui-galleria></div>
// setup(root): properties and events setup(root) { root.querySelector('#gal-auto').items = IMAGES; }

Full screen

full-screen adds a control over the hero that opens the viewer over the whole page, with the strip and the navigators under the picture. The viewer is a modal dialog named by the element's aria-label: focus stays inside, Escape, the scrim and Close close it, and suiClose is cancelable.

<div style="max-width:460px"><sui-galleria id="gal-full" height="200" full-screen aria-label="Invoice 4405"></sui-galleria></div>
// setup(root): properties and events setup(root) { root.querySelector('#gal-full').items = pages(); }

A picture that fails

A picture that fails to load shows the image glyph and "Image unavailable" on the frame's well, and its thumb shows the glyph alone. suiError fires once per failed source with that source, so the host can fetch it again or report it.

Loading.
<div style="max-width:460px;display:flex;flex-direction:column;gap:10px"> <sui-galleria id="gal-fail" height="200" index="1" aria-label="Attachments, INV-1043"></sui-galleria> <span id="gal-fail-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Loading.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#gal-fail'); const log = root.querySelector('#gal-fail-log'); el.addEventListener('suiError', (e) => { log.textContent = `suiError: ${e.detail}`; }); el.items = [ { src: IMAGES[0].src, alt: IMAGES[0].alt, caption: 'Attachment 1 of 3' }, { src: '/docs/assets/imagery/not-uploaded-receipt.jpg', alt: 'Receipt for INV-1043', caption: 'Attachment 2 of 3, the receipt' }, { src: IMAGES[1].src, alt: IMAGES[1].alt, caption: 'Attachment 3 of 3' }, ]; }

Controlled index and events

index and open are mutable props: bind them, and listen to the events. suiIndexChange fires after every thumb, navigator, swipe, arrow-key and autoplay move with the new index; suiOpen and suiClose report the full-screen viewer. A host write emits nothing, so these three buttons refresh the readout themselves.

Page 1 of 12
<div style="max-width:460px;display:flex;flex-direction:column;gap:10px"> <sui-galleria id="gal-controlled" height="200" full-screen aria-label="Invoice 4405"></sui-galleria> <div style="display:flex;gap:10px;align-items:center"> <sui-button variant="outline" size="sm" id="gal-prev" label="Previous"></sui-button> <sui-button variant="outline" size="sm" id="gal-next" label="Next"></sui-button> <sui-button variant="outline" size="sm" id="gal-open" label="Open full screen"></sui-button> <span id="gal-readout" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Page 1 of 12</span> </div> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#gal-controlled'); const out = root.querySelector('#gal-readout'); el.items = pages(); const show = (tail = '') => (out.textContent = `Page ${el.index + 1} of ${el.items.length}${tail}`); root.querySelector('#gal-prev').addEventListener('click', () => { el.index = Math.max(0, el.index - 1); show(); }); root.querySelector('#gal-next').addEventListener('click', () => { el.index = Math.min(el.items.length - 1, el.index + 1); show(); }); root.querySelector('#gal-open').addEventListener('click', () => { el.open = true; show(', full screen'); }); el.addEventListener('suiIndexChange', () => show()); el.addEventListener('suiOpen', () => show(', full screen')); el.addEventListener('suiClose', (e) => show(`, closed by ${e.detail}`)); show(); }

Keyboard

KeyAction
TabMoves through the navigators, the pause control and the full-screen control over the hero, then to the active thumb, the strip's one tab stop.
ArrowLeft ArrowRightShow the previous or next image and move focus to its thumb, stopping at the ends unless circular is set. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one; a swipe follows the same rule.
Home EndShow the first or last image and move focus to its thumb.
Enter SpacePress the focused thumb, navigator or control.
EscapeCloses the full-screen viewer and returns focus to its control. Only the topmost open modal answers it.

API

<sui-galleria>

Properties

PropertyAttributeTypeDefaultDescription
autoplayautoplaynumbernumber | undefinedMilliseconds between automatic advances; unset or 0 leaves the viewer still. Setting it adds a pause control over the hero, the reader's way to stop the motion. It also waits while the pointer is over the element, while focus is inside it, so it waits while the full-screen viewer holds focus, and while the tab is hidden. It stops for good on the first navigation the user makes and never runs for a user who asked for reduced motion. At the last image it wraps when circular is set and stops otherwise.
circularcircularbooleanfalseWraps past either end instead of stopping there. The navigators never disable while it is set.
fullScreenfull-screenbooleanfalseAdds a full-screen control over the hero that opens the viewer over the whole page: the picture fills the viewport on a deep navy scrim with the strip and any navigators under it. open drives the viewer itself, so a host that wants its own trigger can leave this off and write open directly.
heightheightnumber260Hero height in px. The full-screen viewer ignores it and fills the page.
hideArrowshide-arrowsbooleanfalseHides the previous and next navigators over the hero, in both the card and the full-screen viewer. They show by default, as on sui-carousel, which takes the same prop; hide them when the thumbnail strip is enough and the picture should carry no chrome.
indexindexnumber0Index of the hero image. Mutable; updated on thumb click, navigator, swipe, arrow keys and autoplay.
itemsproperty onlySuiGalleriaItem[][]Images: { src, thumbSrc, alt, caption }[]. Property, not attribute.
openopenbooleanfalseShows the full-screen viewer. Mutable; the element opens it from the full-screen control and closes it on Escape, the scrim and the close control. Focus is trapped inside while it is open and returns to the control afterwards. While open, and while it fades out, the viewer paints in the browser's top layer, so a transformed or clipping ancestor cannot trap it; everything outside it is inert and the page does not scroll, and a node the page adds outside it while it is open goes inert too. The viewer is role="dialog" with no aria-modal, so VoiceOver keeps reading the toast region; Tab goes on from the viewer into the toast stack and back. An element the page marks data-sui-keep-live stays usable with everything in it, and focus may move into it and back.

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the full-screen viewer closes itself; detail is 'escape' | 'scrim' | 'close-button'. Cancelable: preventDefault() keeps it open. It does not bubble. The corner X is always there: unlike sui-dialog there is no hide-close, and that is a decision rather than a gap. A dialog may force a choice and a message may be permanent, so those two let the page leave the control out (hide-close, and closable left unset). This is a full-screen viewer over the whole page, so a copy of it with no visible way out is a trap and not a configuration. Cancel the event instead if a close has to be refused.
suiErrorstringonSuiError@sui-errorFires when a picture fails to load, once per source; detail is the source that failed. The picture's place shows the image glyph and "Image unavailable" on the frame's well, and a failed thumbnail the glyph alone, as sui-image draws its own. Does not bubble.
suiIndexChangenumberonSuiIndexChange@sui-index-changeFires after navigation by the user or by autoplay; detail is the new index. Does not bubble.
suiOpenvoidonSuiOpen@sui-openFires when the user opens the full-screen viewer. It does not bubble.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the card's first control that takes it, in tab order: a navigator that is not disabled, the pause control or the full-screen control over the hero, else the current thumb. The host's own focus() does the same. While the full-screen viewer is open, focus is the viewer's and this does nothing.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiGalleriaItem interfaceOne Galleria image: sources, alt text and an optional caption over the hero.
srcstringFull-size source, shown in the hero.
thumbSrc?stringSmall source for the thumbnail strip. Falls back to src, which downloads the full image once per thumb: set it whenever the full images are larger than a preview needs to be.
alt?string
caption?string

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also