Bridge view
Tile
An image-led grid item with minimal supporting text. Used for galleries, asset grids, image picker grids, and any layout where visual content is primary and text is supplementary or absent. Distinct from Card (content-led with hierarchical title+body+media+actions); a wall of Tiles reads as a gallery, a wall of Cards reads as a feed. Tiles are commonly arranged in a grid with consistent aspect ratios and may be interactive (whole-tile activates) or selectable (whole-tile multi-selects).
When to use
Use
For image-led grid items where the visual content is primary — photo galleries, asset pickers, album / playlist grids, profile-picture pickers, document-thumbnail grids, avatar selection. Text is supplementary or absent. Tiles arrange in a regular grid with consistent aspect ratios.
Avoid
For content-led layouts with hierarchical title+body+media — that is `Card`. For dense list rows with metadata — that is `ListItem`. For floating selection of a value from a fixed list — that is `Select`. For interactive inline actions — that is `Button` directly.
Versus related
- card
`Card` is content-led with hierarchical title + body + media + actions; `Tile` is image-led with minimal text. A wall of Cards reads as a feed (each card is read sequentially); a wall of Tiles reads as a gallery (the whole grid is scanned visually). The decision test: is the user scanning for visual content or textual hierarchy?
- list-item
`ListItem` is a horizontal row in a list with leading icon + primary + secondary + trailing affordances; `Tile` is a 2D grid item with media at primary position. List is one-dimensional; tile grid is two-dimensional. ListItem favours dense metadata layouts; Tile favours image-led visual layouts.
Figma↔Code mismatches
Where designer and developer worlds typically misalign on this component.
- 01 Figma
A tile drawn with substantial body text and a header
CodeA Tile component with minimal label only
ConsequenceDesigners may compose tiles with rich text content, multiple lines of body, and headers. Implementations following the design ship a Card semantically — the tile becomes content-led, not image-led. The grid loses its gallery-feel; the canonical reference is misapplied.
CorrectTile is image-led with minimal supporting text. If the surface needs a hierarchical title+body+media composition, use Card. The decision test: is the user scanning for visual content (Tile, gallery) or for textual hierarchy (Card, feed)?
- 02 Figma
Selected tile drawn with a subtle border colour change
CodeSelected tile with border + filled inner overlay + visible checkbox
ConsequenceDesigners use a subtle border colour for selection. Implementations following the design ship colour-only selection — fails WCAG 1.4.1 (Use of Color), and the visible-checkbox affordance is missing so selection discoverability breaks (users may not realise tiles are selectable).
CorrectSelected state is communicated via three layers: the tile root (border + inner-overlay tint), the visible checkbox at one corner (checked state), and the `aria-selected="true"` accessible state. Each layer is reinforcement; remove any one and the others still communicate selection.
- 03 Figma
Text label drawn directly on image without a contrast scrim
CodeText label rendered with WCAG-passing contrast via overlay scrim
ConsequenceDesigners compose text-on-image mocks against idealised dark or light backgrounds. Implementations following the design ship arbitrary user-uploaded images; some have low-contrast regions and the label text becomes unreadable. WCAG 1.4.3 (Contrast Minimum) fails for those images.
CorrectFor text-on-image labels (label inside the tile, overlaying media), always include the overlay scrim. Document in the canonical reference that the overlay is a contrast-affordance, not a stylistic choice. Allow implementations to skip the overlay only for separated-caption labels (label below media, no overlap).
- 04 Figma
Aspect ratio drawn at fixed pixels rather than as a ratio property
CodeAspect ratio modelled as a property with logical values
ConsequenceDesigners fix tile pixel dimensions in mocks (240×240, 240×320). Implementations following the design hard-code pixel values; tile grids do not scale to viewport changes (zoom, screen size, density variations). Or developers ignore the design and ship CSS aspect-ratio; the design and production diverge on tile shape.
CorrectDocument `aspect` as a first-class property with logical values (`square`, `portrait`, `landscape`, `free`). The Figma component carries it as a Variant; CSS uses `aspect-ratio` plus auto inline-size. Pixel dimensions are not canonical.
Variants, properties, states
Variants
Structurally different versions of the component.
standardinteractiveselectable Properties
The same component, parameterised.
| Property | Type |
|---|---|
aspect | square | portrait | landscape | free |
hasOverlay | boolean |
hasLabel | boolean |
density | comfortable | compact |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | idleselected |
Figma ↔ Code property map
| Figma | Type | Code | Notes |
|---|---|---|---|
Variant | Variant | variant | Maps standard / interactive / selectable. Drives root semantic (container vs button vs option). |
Aspect | Variant | aspect | square / portrait / landscape / free. Drives CSS `aspect-ratio` on the media slot. |
Density | Variant | density | comfortable / compact. |
Has Overlay | Boolean | hasOverlay | Toggles the contrast-scrim overlay. Required when label is text-on-image; optional for separated-caption layouts. |
Has Label | Boolean | hasLabel | Toggles the label slot. Tiles in image-only galleries (artwork grids) may omit labels entirely. |
Has Badge | Boolean | hasBadge | — |
Selected | Boolean | selected | For selectable variant. Drives `aria-selected` plus the visible checkbox state. |
Media | Instance Swap | media | Swap the media component instance — image, video, or rendered preview. |
Badge | Instance Swap | badge | Swap the badge component instance — status pill, count, "new" pulse. |
State transitions
| From | To | Trigger |
|---|---|---|
idle | selected | User activates the tile (click on `selectable` variant, or click on the visible select-affordance checkbox). `aria-selected="true"` on the tile; `aria-checked="true"` on the checkbox; visual treatment changes. |
selected | idle | User activates the tile or checkbox again, or consumer programmatically clears selection. Visual treatment returns to default. |
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | frame | Aspect-ratio-locked frame; corner radius and shadow from variant |
media | frame | Aspect-ratio-locked image fill; cover or contain by variant |
overlay | rectangle | Gradient fill from transparent to dark at the bottom edge for label legibility |
label | text | Caption text style; truncates with ellipsis when overflowing |
badge | instance | Badge component instance; positioned absolutely at one corner of the tile |
select-affordance | instance | Checkbox component instance; visibility bound to selectable variant + selected state |
Code anatomy
| Slot | Code slot | Semantic |
|---|---|---|
root | root | container-or-button-or-checkbox |
media | media | img-or-video |
overlay | overlay | presentational |
label | label | text |
badge | badge | presentational-or-status |
select-affordance | select | checkbox |
Cross-framework expression
| Framework | Structure mechanism | Variant mechanism |
|---|---|---|
| Web Components | A `<ui-tile>` host with named slots for media, label, badge, select; light DOM for the media (consumer provides `<img>` or `<video>`); root may be `<button>` or styled element depending on variant | attributes (`variant="selectable"`, `aspect="square"`, `density="comfortable"`); `data-state="idle|selected"` for CSS |
| React | Single `<Tile>` component with compound subcomponents (`Tile.Media`, `Tile.Label`, `Tile.Badge`, `Tile.Select`) or named props for content; integrates with `useGridList` (React Aria) for grid-pattern selectable usage | props with class-variance-authority for variant / aspect / density; controlled or uncontrolled selected state |
| Angular (signals) | `<ui-tile>` component with content projection for media, label, badge slots; signal-based selected state; integrates with Angular CDK Listbox or custom grid composition | input<'standard' | 'interactive' | 'selectable'>(); input<'square' | 'portrait' | 'landscape' | 'free'>(); `[(selected)]` two-way binding |
| Vue | Custom `<Tile>` SFC with named slots for media, label, badge, select; integrates with manual grid keyboard composition | defineProps with literal-union types; `:variant`, `:aspect` props; emits `update:selected` |
Events
clickActivateselectedChange
Form integration
- name attribute
- Tile itself does not carry a form `name` attribute. The visible select-affordance checkbox (when selectable variant is used inside a form) carries the `name` and `value` — typically `name=`tile-ids[]`, value=<tileId>` for multi- select forms. Consumers wrap the selectable tile grid in a `<fieldset>` with a `<legend>` describing the multi-select purpose.
- FormData serialization
- Multi-select tile grid produces one FormData entry per selected tile (matching `<input type="checkbox">` and `<select multiple>` behaviour). Canonical serialization is `name=tile-ids[]&value=<tileId>` for each selected tile. Avoid comma-joining tile ids into a single FormData entry.
- form.reset()
- `form.reset()` restores tiles to their `defaultChecked` state (typically all unchecked). The visual selected state clears alongside; `aria-selected` reverts. Framework state held outside the DOM needs an `onReset` listener to sync.
- HTML5 validation
- Tile grids with `required: true` (at least one must be selected, e.g. avatar picker) trigger HTML5 validation on submit via the inner checkboxes. The first checkbox in the grid acts as the validation target; `setCustomValidity()` on it surfaces "select at least one" via `aria-invalid`. For more sophisticated validation (exactly N, between M and N), use form-state libraries with custom validators.
Performance thresholds
lazyLoadOffscreenThresholdviewport-distance≥200pxTiles in the viewport (and within ~200px of viewport edges) load eagerly; tiles further away load lazily via `loading="lazy"` on the image element or IntersectionObserver-driven loading. The 200px buffer avoids visible "popping in" at the viewport edge while saving bandwidth and initial render cost.
tilesPerRowMobilecolumn-count≥2columnsBelow `breakpoint.sm`, the canonical maximum is 2 tiles per row regardless of authored grid configuration. Beyond 2 on narrow viewports tiles become too small to preview meaningfully; the gallery loses its image-led character.
tilesPerRowDesktopcolumn-count≥6columnsAbove `breakpoint.md` for image-heavy galleries, up to ~6 tiles per row at typical desktop widths (1280-1920px) maintain visual scannability. Beyond 6 tiles preview content becomes too small; consumers either reduce column count or design at smaller-tile-density. The threshold is canonical-recommended, not hard-enforced.
Internationalisation
RTL · mirroring
Tile content is direction-neutral when image-only. Labels inherit document direction. Select-affordance checkbox moves from inline-start (visual left in LTR) to inline-start (visual right in RTL). Badge position (one of four corners) is consumer-configured; the canonical default places it at inline-end-top, which mirrors visually. Hover-lift animation is direction- neutral.
Text expansion
Tile labels grow with translation; canonical convention is single-line truncation with ellipsis, full text in `aria-label` for SR fallback. For tile galleries with long labels, consider switching to Card (which accommodates multi-line title + body). Tile aspect ratios are unchanged by text expansion.
Accessibility
| Slot | Accessibility hint | |
|---|---|---|
root | For `variant: standard`, root has no role (just a visual container). For `variant: interactive`, root is a `<button>` (or `<a href>` for tiles that navigate) carrying the accessible name from the label slot. For `variant: selectable`, root carries `aria-selected` plus `role="option"` when nested in a `role="listbox"` parent grid; for grid-pattern selectable tiles (canonical for bulk selection workflows) use `role="gridcell"` inside a `role="grid"`. | |
media | Provide alt text for informative images; `alt=""` for purely decorative tiles where the label carries the meaning. Avoid duplicating the label in alt text — SR users would hear the same text twice. For video, provide captions or a transcript link. | |
overlay | Decorative; do not put role on it. The overlay's job is contrast, not content. SR users do not encounter it. | |
label | Plain text. For interactive tiles, the label is the accessible name (composed via `aria-labelledby` or wrapping inside the activator element). For text-on- image label position, ensure WCAG 1.4.3 (Contrast Minimum) against the image is met via the overlay scrim — never rely on a single image's local contrast. | |
badge | Decorative when state is also conveyed via aria-state (selected, current). Informational when carrying unique meaning — append the meaning to the tile's accessible name via visually-hidden text ("Verified profile"). Never rely on the badge alone for state communication. | |
select-affordance | Visible checkbox is the canonical form (preferred over click-anywhere-to-select-without-affordance). The checkbox carries its own `aria-checked`; the tile's `aria-selected` mirrors the checked state. Some implementations skip the visible checkbox and rely on the tile's selection-state visual treatment alone — the canonical recommendation is to render the checkbox so the affordance is discoverable. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab | For a single tile, focus enters and exits via Tab in document order. For selectable tile grids, Tab focuses the currently-focused tile (roving tabindex) and subsequent Tab leaves the grid — ArrowKeys navigate within. |
Enter or Space (focus on interactive or selectable tile) | Activates the tile — fires the click handler (interactive variant) or toggles selection (selectable variant). For tiles wrapping `<a>`, Enter only (Spacebar is button-only convention). |
ArrowKeys (focus inside selectable grid) | Move focus to neighbouring tiles in 2D grid coordinates per APG grid pattern. ArrowDown jumps a row; ArrowRight jumps a column; Home / End move to the row's start / end; Ctrl+Home / Ctrl+End jump to grid start / end. |
Space (selectable variant, focus on tile) | Toggles selected state. `aria-selected` flips on the tile; `aria-checked` flips on the visible checkbox if present. |
Screen-reader announcements
| Trigger | Expected |
|---|---|
| Focus enters interactive tile | SR announces the tile's accessible name (composed from label + optional badge meaning) followed by "button" or "link" depending on the activator element. Selectable tiles announce additionally "selected" or "not selected". |
| Selected state toggles | SR announces the new state ("<tile name>, selected" or "<tile name>, not selected"). Driven by `aria-selected` flipping on the tile or `aria-checked` on the checkbox. |
| Tile activated (interactive variant) | SR announces what the activation does — typically the next page's title (for navigation tiles) or the confirmation of the action (for action tiles). Implementation detail: focus management may move focus to a detail panel or to a route-target heading. |
axe-core rules to assert
aria-allowed-rolearia-required-attraria-required-childrenaria-required-parentcolor-contrastimage-altrole-img-altlink-namebutton-name
Common mistakes
#tile-as-card
Tile used for content-led layout
The component is used to render a hierarchical title+body+media composition (a feed of articles, a list of products with descriptions). Semantically this is Card; using Tile collapses the visual hierarchy and the grid feels like a wall of unread content rather than a gallery.
Use Card for content-led compositions. Tile is for image-first content where the visual is the message and text is at most a label. The "wall of Tiles vs wall of Cards" test from `whenToUse.vsRelated[card]` is the canonical guide.
#tile-no-aspect-ratio
Tile sized intrinsically by image dimensions
Implementation does not enforce aspect ratio; tiles take their natural image dimensions. Grid layout breaks — tiles vary in height (or width), the gallery becomes ragged, and visual scanning is harder.
Enforce aspect ratio via CSS `aspect-ratio: 1`, `aspect-ratio: 3/4`, etc. matching the `aspect` property. Inside, image fits via `object-fit: cover` (or `contain` for non-cropping tiles). The grid stays regular regardless of source image dimensions.
#tile-selected-color-only
Selection differentiated by colour alone
Selected tiles have a coloured border or background tint; nothing else changes. Colour-vision deficient users miss the state; SR users hear no `aria-selected` cue.
Triple-layer selection (visual + checkbox-affordance + `aria-selected`). The visible checkbox at one corner is the canonical discoverability affordance — without it users may not realise tiles are selectable.
#tile-text-overlay-no-contrast
Text label on image without contrast scrim
Label rendered directly on the image with no intermediate gradient or scrim. Against varied user- uploaded images, contrast falls below WCAG 1.4.3 (4.5:1 for normal text); some images render the label effectively invisible.
Include the overlay scrim for any text-on-image label pattern. The scrim is a constant gradient (transparent → dark at the bottom edge) that guarantees minimum contrast against any image. Document the scrim as mandatory for text-on-image; optional or omitted for separated-caption variants.
#tile-keyboard-no-roving-tabindex
Selectable tile grid with no roving tabindex
Implementation puts `tabindex="0"` on every tile in a selectable grid. Tab cycles through every tile; for a grid of 50 photos, that is 50 extra tab stops. APG grid pattern uses roving tabindex — only one tile is in the tab order, ArrowKeys navigate between tiles.
Apply roving tabindex per APG grid pattern — only the currently-focused tile has `tabindex="0"`, others have `tabindex="-1"`. ArrowLeft / Right / Up / Down move focus between tiles in 2D grid coordinates. Tab moves focus out of the grid to the next document focusable.