Skip to content
ShineUI
Install

Image

Click to look closer.

<sui-image>MediaStable

About

Image with a hover veil and a click-to-preview lightbox: the caption and the zoom, rotate and close tools sit in a bar over a dark scrim, and the picture pans in the area under it. For receipts, attached documents and team photos. Set no-preview for a plain framed picture.

Usage

<sui-image src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" caption="Attached photo. Click to preview." width="240" height="160"></sui-image> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-image"); el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiImage } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiImage src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" caption="Attached photo. Click to preview." width="240" height="160" onSuiClose={(e) => console.log(e.detail)} />; }
<script setup> import { SuiImage } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiImage src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" caption="Attached photo. Click to preview." width="240" height="160" @sui-close="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiImage } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiImage], template: `<sui-image src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" caption="Attached photo. Click to preview." width="240" height="160" [(open)]="open" (suiClose)="onChange($event)"></sui-image>`, }) export class Example { open = false; onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The frame is a button named "Preview:" plus the caption or alt. Hovering shows the navy veil; a click opens the lightbox with zoom, rotate and close on a deep navy scrim, and the caption repeats under it.

<sui-image src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" caption="Attached photo. Click to preview." width="240" height="160"></sui-image>

Sizes and fit

width and height take a number of px or any CSS length. cover crops to fill the frame; contain fits the whole picture inside it, which a portrait receipt in a landscape frame needs.

<div style="display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start"> <sui-image src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" width="120" height="80"></sui-image> <sui-image src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" width="180" height="120" radius="4px" caption="radius=4px"></sui-image> <sui-image src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" width="180" height="120" fit="contain" caption="fit=contain"></sui-image> </div>

Preview source

preview-src is what the lightbox enlarges when the frame should show something smaller: a thumbnail in the list, the full scan on demand. The frame keeps src, and a preview source that fails is reported through suiError and leaves the frame a plain picture.

<sui-image src="/docs/assets/imagery/construction.webp" preview-src="/docs/assets/imagery/real-estate.webp" alt="A hard hat resting on rolled plans" caption="Thumbnail in the frame, full picture in the lightbox" width="240" height="160"></sui-image>

Plain image

no-preview renders the frame with no veil, no button and no lightbox.

<sui-image src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" no-preview width="240" height="160" caption="Property photo"></sui-image>

Controlled lightbox and events

open is a mutable, reflected prop; suiOpen fires when the user opens the lightbox. suiClose is cancelable and names its reason, so a stray scrim click can be vetoed while Escape and the close control still work.

Closed
<div style="display:flex;flex-direction:column;gap:10px;align-items:flex-start"> <sui-image id="img-controlled" src="/docs/assets/imagery/real-estate.webp" alt="An aerial view of a suburban street lined with houses and lawns" caption="Attached photo" width="240" height="160"></sui-image> <div style="display:flex;gap:10px;align-items:center"> <sui-button variant="outline" size="sm" id="img-open" label="Open preview"></sui-button> <sui-checkbox id="img-guard" label="Ignore scrim clicks"></sui-checkbox> </div> <span id="img-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Closed</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#img-controlled'); const log = root.querySelector('#img-log'); const guard = root.querySelector('#img-guard'); root.querySelector('#img-open').addEventListener('click', () => (el.open = true)); el.addEventListener('suiOpen', () => (log.textContent = 'Open')); el.addEventListener('suiClose', (e) => { if (guard.checked && e.detail === 'scrim') { e.preventDefault(); log.textContent = 'Scrim click ignored'; return; } log.textContent = `Closed by ${e.detail}`; }); }

A source that fails

The error slot replaces a picture that fails, as on the first frame. With the slot empty the frame shows the image glyph and "Image unavailable" on its well, as on the second. suiError reports the source, and the frame stops being a preview button, because there is nothing to enlarge.

Attachment unavailable
Loading.
<div style="display:flex;flex-direction:column;gap:10px;align-items:flex-start"> <div id="img-broken" style="display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start"> <sui-image src="/docs/assets/imagery/not-uploaded.jpg" alt="Missing attachment" width="240" height="160" caption="Invoice 4405"> <span slot="error" style="font:460 12.5px/1.6 var(--font-sans);color:var(--ink-faint)">Attachment unavailable</span> </sui-image> <sui-image src="/docs/assets/imagery/not-uploaded-receipt.jpg" alt="Receipt for INV-1043" width="240" height="160" caption="Receipt, INV-1043"></sui-image> </div> <span id="img-error-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 log = root.querySelector('#img-error-log'); const failed = []; root.querySelector('#img-broken').addEventListener('suiError', (e) => { failed.push(e.detail); log.textContent = `suiError: ${failed.join(', ')}`; }); }

Keyboard

KeyAction
Enter SpaceOn the frame, opens the lightbox. On a tool, presses it.
TabMoves through the zoom, rotate and close tools. Focus stays inside the lightbox while it is open.
Arrow keysPan the enlarged picture once it is zoomed past 1x.
EscapeCloses the lightbox and returns focus to the frame. Only the topmost open modal answers it.

API

<sui-image>

Properties

PropertyAttributeTypeDefaultDescription
altaltstring''Alt text. Also names the lightbox when there is no caption.
captioncaptionstringstring | undefinedCaption under the image (11.5px faint ink) and under the lightbox preview.
fitfitSuiImageFit"contain" | "cover"'cover'How the picture fills the frame: cover crops to fill it, contain fits the whole picture inside it. A portrait receipt in a landscape frame loses its top and bottom under cover. Unknown values fall back to cover.
heightheightnumber | string | undefinedFrame height: a number of px or any CSS length.
loadingloadingSuiImageLoading"eager" | "lazy"'lazy'Native loading hint for the framed picture. lazy by default, because the shape this element is for is a list of attachments. The enlarged preview always loads eagerly, because the user asked for it. Unknown values fall back to lazy.
noPreviewno-previewbooleanfalseTurns the preview off, for a plain framed picture. By default the frame is a button: hovering shows the navy veil with a zoom glyph, and a click, Enter or Space opens the lightbox. With no-preview there is no veil, no button and no lightbox.
openopenbooleanfalseShows the lightbox. Mutable; the element opens it on click and closes it on Escape, the scrim and the close control. Focus is trapped inside while it is open and returns to the frame afterwards. Zoomed past 1x, the picture drags to pan and the arrow keys pan it too. While open, and while it fades out, the lightbox 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 lightbox is role="dialog" with no aria-modal, so VoiceOver keeps reading the toast region; Tab goes on from the lightbox 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.
previewSrcpreview-srcstringstring | undefinedSource for the enlarged preview, when it should not be src: a full-resolution scan behind a small framed thumbnail, so a list of fifty receipts downloads fifty thumbnails and the one scan the reader opens. Falls back to src. If it fails to load the preview withdraws itself and the frame stops being a preview button, while the framed picture stays.
radiusradiusstringstring | undefinedFrame corner radius (default var(--radius-lg)).
src*srcstringImage source.
widthwidthnumber | string | undefinedFrame width: a number of px or any CSS length.

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the lightbox closes itself; detail is 'escape' | 'scrim' | 'close-button', or 'cancel' when the source failed while the preview was open. 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). A lightbox covers the 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 src or previewSrc fails to load; detail is the source that failed. A failed src is replaced by the error slot, or, with nothing in it, by the image glyph and "Image unavailable" on the frame's well, as the galleria and the image compare draw theirs. Either failure leaves the frame a plain picture rather than a preview button, because there is nothing to enlarge. Does not bubble.
suiOpenvoidonSuiOpen@sui-openFires when the user opens the lightbox. It does not bubble.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the frame, the preview button. A plain picture (no-preview, or a source that failed) has no tab stop and keeps focus where it is. The host's own focus() does the same.

Slots

SlotDescription
errorShown in place of the picture when src fails to load. Empty by default.

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.
SuiImageFit typeHow the picture fills its frame.
No members.
SuiImageLoading typeNative loading hint for the framed picture.
No members.

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