Components · Data Display
Image
A framed image with rounded corners, a hover preview mask, a loading placeholder and a broken-src fallback.
Examples
Preview
Radius
Source
Image · rounded frame · hover to preview · click to open
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-image> is the SAME shared custom element React and Vue consume — here in its
native form. Theming comes only from the --aha-* tokens in tokens.css. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-image.js'; // registers <aha-image>
</script>
<!-- Click-to-zoom preview (default), rounded frame -->
<aha-image width="240" height="150" src="https://picsum.photos/240/150" alt="Deck cover">Preview</aha-image>
<!-- Plain framed picture — no zoom -->
<aha-image width="240" height="150" preview="false" radius="lg" src="https://picsum.photos/240/150" alt="Cover"></aha-image>
<!-- Loading placeholder + a broken-src fallback -->
<aha-image width="240" height="150" placeholder src="https://picsum.photos/240/150" alt="Cover"></aha-image>
<aha-image width="240" height="150" fallback="Couldn’t load cover" src="/does-not-exist.png" alt="Cover"></aha-image>
import '@ahaslides-product/design/aha-image'; // registers <aha-image>
// The custom element renders its own shadow DOM; React just passes attributes + the mask label.
// `preview="false"` → a plain framed picture; `placeholder` shows a shimmer; a broken src falls back.
function Cover({ src, alt, label = 'Preview', preview = true, radius = 'default' }) {
return (
<aha-image
src={src} alt={alt} width={240} height={150}
radius={radius} placeholder
preview={preview ? 'true' : 'false'}
fallback="Couldn’t load image"
>{label}</aha-image>
);
}
// usage
<Cover src="https://picsum.photos/240/150" alt="Deck cover" />
<Cover src="https://picsum.photos/240/150" alt="Logo" preview={false} radius="lg" />
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-image';
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<template>
<!-- Click-to-zoom preview (default) -->
<aha-image
src="https://picsum.photos/240/150"
alt="Deck cover"
width="240"
height="150"
radius="lg"
placeholder
fallback="Couldn’t load image"
>Preview</aha-image>
<!-- Plain framed picture — no zoom -->
<aha-image
src="https://picsum.photos/240/150"
alt="Logo"
width="240"
height="150"
preview="false"
></aha-image>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
src | string | — | Image source URL (empty/broken → the fallback state) |
alt | string | — | Alternative text for the image |
width | number | — | Frame width in px |
height | number | — | Frame height in px |
preview | 'true' | 'false' | true | Click-to-zoom modal. `false` → a plain framed picture (no mask, no click, no dialog) |
radius | 'xs'|'sm'|'default'|'lg'|'xl'|'pill' | default | Frame corner from the DS radius scale |
placeholder | boolean | off | Show a shimmer loading skeleton until the image loads |
fallback | string | Image unavailable | Label shown in the broken-src error state (its presence never forces the state — a broken/empty src does) |
(default slot) | slot | Preview | The label shown in the hover mask |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-image'; // registers <aha-image>
Agent feed for this component (absolute, fetchable anywhere): image.agent.json · image.md · image.llms.txt
When to use
When to use
- Image — a framed picture that invites a click to preview (a deck cover, an upload)
- Avatar — a small round identity image
- Card — a picture is one part of a larger grouped surface
Always set alt for accessibility. Keep the mask label short — 'Preview', 'View' — it is an invitation, not a caption.
Surfaces
editor dashboard audience
Spec
Frame radius 8px (radius attr xs 4·sm 6·default 8·lg 12·xl 16·pill) · bg #F7F7F7 · overflow hidden · role=button, keyboard-focusable · Image object-fit cover · scale 1.06 on hover, motion-slow ease-out · Mask overlay rgba(26,26,46,.7) · #FFFFFF 13/SemiBold · fade motion-mid · Preview click/Enter opens a modal dialog (role=dialog, aria-modal); Esc / close button / backdrop dismiss; focus moves in and is restored on close. preview="false" → plain picture, no mask/click/dialog · Placeholder shimmer skeleton (icon-muted on bg-secondary) shown while loading; fades on the image load, persistent node · Fallback broken/empty src → system-image-square glyph + label, text-tertiary on bg-secondary, role=img · Sizing width/height attributes drive the frame