Image Tile
- Stable
- WCAG 2.2 evidence
- RTL
A square picture in a gallery or picker, with overlay actions on hover and a selected state. The tile itself is one button: a React Aria ToggleButton or a Button when onPress is set. Its name is
the picture's alt (or aria-label). The overlay actions are sibling buttons after it in the tab order, each described
by the tile's name. Keyboard: Tab reaches the tile and then its actions; Enter and Space press them; keyboard focus shows the focus ring and
reveals the actions (WCAG 2.1.1, 2.4.7, 4.1.2). Pointer: hover reveals the actions over a 45 % media/bg scrim; Escape
hides that hover overlay again (1.4.13); on touch devices the actions are always visible. Targets: the tile, and 36 px
action buttons (2.5.8). Hover, focus and selection are driven by React Aria (data-hovered, data-focus-visible,
data-selected, data-actions-visible on the root). Consumer duties: meaningful alt, short translated action labels, confirming destructive actions (Delete), and a
visible summary of a multiple selection where it matters.
import { ImageTile } from "@nexera-ui/react";- 5
- examples
- 13
- props
- 3
- live controls
- 1
- platform
- 11
- WCAG criteria
- 2
- blocks use it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { ImageTile } from "@nexera-ui/react";
<ImageTile />Examples 4
The same examples as Storybook, rendered live. Open Code to copy one.
Selectable gallery
A picker: tiles in a wrapping grid, selection kept by the parent (controlled), with a visible summary.
import { useState } from "react";
import { ImageTile } from "@nexera-ui/react";
import { LuDownload, LuTrash2, LuZoomIn } from "react-icons/lu";
const scene = (sky: string, ground: string) =>
`data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><rect width="300" height="300" fill="${sky}"/>` +
`<circle cx="210" cy="80" r="30" fill="white" fill-opacity="0.85"/>` +
`<path d="M0 220 L90 140 L160 200 L220 150 L300 220 L300 300 L0 300 Z" fill="${ground}"/></svg>`,
)}`;
const PHOTOS = [
{ id: "a", src: scene("navajowhite", "peru"), alt: "Team photo at the March offsite" },
{ id: "b", src: scene("lightsteelblue", "seagreen"), alt: "Open-plan office, second floor" },
{ id: "c", src: scene("lightblue", "darkolivegreen"), alt: "Hills behind the retreat venue" },
{ id: "d", src: scene("thistle", "slateblue"), alt: "Welcome desk at reception" },
] as const;
const actionsFor = (name: string): ImageTileAction[] => [
{ label: "Zoom in", icon: <LuZoomIn />, onPress: () => undefined, id: `${name}-zoom` },
{ label: "Download", icon: <LuDownload />, onPress: () => undefined, id: `${name}-download` },
{ label: "Delete", icon: <LuTrash2 />, onPress: () => undefined, id: `${name}-delete` },
];
export function SelectableGallery() {
function Gallery() {
const [selected, setSelected] = useState<ReadonlySet<string>>(new Set(["b"]));
return (
<section className="flex max-w-3xl flex-col gap-3" aria-labelledby="gallery-title">
<h2 id="gallery-title" className="m-0 text-heading-h4 text-primary">
Offsite photos
</h2>
<p className="m-0 text-body-small text-secondary" aria-live="polite">
{selected.size} of {PHOTOS.length} selected
</p>
<ul className="m-0 grid list-none grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-3 p-0">
{PHOTOS.map((photo) => (
<li key={photo.id}>
<ImageTile
src={photo.src}
alt={photo.alt}
isSelected={selected.has(photo.id)}
onChange={(on) => {
setSelected((current) => {
const next = new Set(current);
if (on) next.add(photo.id);
else next.delete(photo.id);
return next;
});
}}
actions={actionsFor(photo.id)}
/>
</li>
))}
</ul>
</section>
);
}
return <Gallery />;
}
Pressable
With onPress the tile is a plain button (for example to open a preview); no selected state.
import { ImageTile } from "@nexera-ui/react";
const scene = (sky: string, ground: string) =>
`data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><rect width="300" height="300" fill="${sky}"/>` +
`<circle cx="210" cy="80" r="30" fill="white" fill-opacity="0.85"/>` +
`<path d="M0 220 L90 140 L160 200 L220 150 L300 220 L300 300 L0 300 Z" fill="${ground}"/></svg>`,
)}`;
const PHOTOS = [
{ id: "a", src: scene("navajowhite", "peru"), alt: "Team photo at the March offsite" },
{ id: "b", src: scene("lightsteelblue", "seagreen"), alt: "Open-plan office, second floor" },
{ id: "c", src: scene("lightblue", "darkolivegreen"), alt: "Hills behind the retreat venue" },
{ id: "d", src: scene("thistle", "slateblue"), alt: "Welcome desk at reception" },
] as const;
export function Pressable() {
return (
<ImageTile
src={PHOTOS[1].src}
alt={PHOTOS[1].alt}
onPress={() => undefined}
className="max-w-50"
/>
);
}
Disabled
Disabled: dimmed, and the tile and its actions leave the tab order.
import { ImageTile } from "@nexera-ui/react";
import { LuDownload, LuTrash2, LuZoomIn } from "react-icons/lu";
const scene = (sky: string, ground: string) =>
`data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><rect width="300" height="300" fill="${sky}"/>` +
`<circle cx="210" cy="80" r="30" fill="white" fill-opacity="0.85"/>` +
`<path d="M0 220 L90 140 L160 200 L220 150 L300 220 L300 300 L0 300 Z" fill="${ground}"/></svg>`,
)}`;
const PHOTOS = [
{ id: "a", src: scene("navajowhite", "peru"), alt: "Team photo at the March offsite" },
{ id: "b", src: scene("lightsteelblue", "seagreen"), alt: "Open-plan office, second floor" },
{ id: "c", src: scene("lightblue", "darkolivegreen"), alt: "Hills behind the retreat venue" },
{ id: "d", src: scene("thistle", "slateblue"), alt: "Welcome desk at reception" },
] as const;
const actionsFor = (name: string): ImageTileAction[] => [
{ label: "Zoom in", icon: <LuZoomIn />, onPress: () => undefined, id: `${name}-zoom` },
{ label: "Download", icon: <LuDownload />, onPress: () => undefined, id: `${name}-download` },
{ label: "Delete", icon: <LuTrash2 />, onPress: () => undefined, id: `${name}-delete` },
];
<ImageTile
src={PHOTOS[0].src}
alt={PHOTOS[0].alt}
className="max-w-50"
actions={actionsFor("default")}
isDisabled
/>;
Unavailable
Without a src, or when it fails, the tile shows the "Image unavailable" frame or the fallback slot.
import { ImageTile } from "@nexera-ui/react";
import { LuDownload, LuTrash2, LuZoomIn } from "react-icons/lu";
const actionsFor = (name: string): ImageTileAction[] => [
{ label: "Zoom in", icon: <LuZoomIn />, onPress: () => undefined, id: `${name}-zoom` },
{ label: "Download", icon: <LuDownload />, onPress: () => undefined, id: `${name}-download` },
{ label: "Delete", icon: <LuTrash2 />, onPress: () => undefined, id: `${name}-delete` },
];
export function Unavailable() {
return (
<div className="flex flex-wrap gap-4">
<ImageTile
className="max-w-50"
actions={actionsFor("default")}
src={null}
alt="Photo not uploaded yet"
/>
<ImageTile
className="max-w-50"
actions={actionsFor("default")}
src={null}
alt="Team lunch"
fallback={
<svg viewBox="0 0 200 200">
<rect width="200" height="200" className="fill-status-warning-bg" />
<circle cx="100" cy="100" r="40" className="fill-status-warning-border" />
</svg>
}
/>
</div>
);
}
Props 13
Press "Try it" on a card to load that prop into the playground.
13 props shown
srcNexerastring | nullImage URL (see Image). Without one, or when it fails, the tile shows fallback or the "Image unavailable" frame.
alt*NexerastringText alternative of the picture, required (WCAG 1.1.1). It also names the tile's button, so describe the picture
("Team at the March offsite"); for a picture without a useful description, pass "" and set aria-label.
fallbackNexeraReactNodeThe Figma Photo placeholder slot: shown without a src or when the picture fails (see Image).
errorLabelNexerastringText of the default error frame. Translate it.
actionsNexerareadonly ImageTileAction[]Overlay actions: round buttons over a media/bg scrim. They appear while
the tile is hovered or keyboard focus is on the tile or an action, are always shown on touch devices, and stay
reachable by Tab and screen readers. Escape hides the hover overlay.
isDisabledNexerabooleanDisables the tile and its actions.
aria-labelNexerastringAccessible name of the tile's button when alt does not name it well. Translate it.
classNameNexerastringExtra classes for the root, merged last so they win over the defaults.
styleNexeraCSSPropertiesInline style for the root.
isSelectedNexerabooleanWhether the tile is selected. Pair it with onChange.
Only on a selectable tile (without onPress).
defaultSelectedNexerabooleanWhether the tile is selected at first render (uncontrolled).
Only on a selectable tile (without onPress).
onChangeNexera(isSelected: boolean) => voidCalled with the new selection when the tile is pressed.
Only on a selectable tile (without onPress).
onPressNexera(event: PressEvent) => voidUse onChange on a selectable tile.
Called when the tile is pressed (pointer, Enter or Space). The tile is a plain button: no aria-pressed.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
8 direct · 3 supporting- 1.1.1Non-text ContentLevel A · tested directly
- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.3.2Meaningful SequenceLevel A · supporting test
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.11Non-text ContrastLevel AA · supporting test
- 1.4.13Content on Hover or FocusLevel AA · tested directly
- 2.1.1KeyboardLevel A · tested directly
- 2.4.3Focus OrderLevel A · tested directly
- 2.4.7Focus VisibleLevel AA · tested directly
- 2.5.8Target Size (Minimum)Level AA · supporting test
- 4.1.2Name, Role, ValueLevel A · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
data-disableddata-hovereddata-pressed
<ImageTile className="data-disabled:opacity-90 shadow-sm" />Used in blocks
Related components
- CarouselA carousel: slides in a scroll-snap strip with Previous / Next, indicators and, when you opt in, automatic rotation with a visible pause button.
- CarouselControlsPrevious / Next buttons for a carousel, and optionally a button that stops and starts automatic rotation.
- CarouselIndicatorShows which slide of a carousel is current, and lets people jump to a slide.
- ImageA picture in a rounded frame with a fixed aspect ratio: Figma `State=Loading` (a `bg/sunken` frame with a shimmer), `Loaded` and `Error` ("Image unavailable") follow the load lifecycle of the `<img>`; they are not props.
- VideoControlsThe control bar of a video: scrubber, play / pause, back, mute and volume, time, captions, settings, picture in picture and full screen, over a dark gradient.
- VideoPlayerA video with a poster, Nexera controls and captions.