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.

Highlight
Fig 1.1 · Tile · Bridge view
Both

Figma↔Code mismatches

Where designer and developer worlds typically misalign on this component.

  1. 01
    Figma

    A tile drawn with substantial body text and a header

    Code

    A Tile component with minimal label only

    Consequence

    Designers 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.

    Correct

    Tile 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)?

  2. 02
    Figma

    Selected tile drawn with a subtle border colour change

    Code

    Selected tile with border + filled inner overlay + visible checkbox

    Consequence

    Designers 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).

    Correct

    Selected 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.

  3. 03
    Figma

    Text label drawn directly on image without a contrast scrim

    Code

    Text label rendered with WCAG-passing contrast via overlay scrim

    Consequence

    Designers 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.

    Correct

    For 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).

  4. 04
    Figma

    Aspect ratio drawn at fixed pixels rather than as a ratio property

    Code

    Aspect ratio modelled as a property with logical values

    Consequence

    Designers 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.

    Correct

    Document `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.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

standardinteractiveselectable

Properties

The same component, parameterised.

PropertyType
aspectsquare | portrait | landscape | free
hasOverlayboolean
hasLabelboolean
densitycomfortable | compact

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
idleselected
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps standard / interactive / selectable. Drives root semantic (container vs button vs option).
AspectVariantaspectsquare / portrait / landscape / free. Drives CSS `aspect-ratio` on the media slot.
DensityVariantdensitycomfortable / compact.
Has OverlayBooleanhasOverlayToggles the contrast-scrim overlay. Required when label is text-on-image; optional for separated-caption layouts.
Has LabelBooleanhasLabelToggles the label slot. Tiles in image-only galleries (artwork grids) may omit labels entirely.
Has BadgeBooleanhasBadge
SelectedBooleanselectedFor selectable variant. Drives `aria-selected` plus the visible checkbox state.
MediaInstance SwapmediaSwap the media component instance — image, video, or rendered preview.
BadgeInstance SwapbadgeSwap the badge component instance — status pill, count, "new" pulse.
Both

State transitions

FromToTrigger
idleselectedUser 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.
selectedidleUser activates the tile or checkbox again, or consumer programmatically clears selection. Visual treatment returns to default.
Designer

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
Dev

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
Dev

Cross-framework expression

FrameworkStructure mechanismVariant 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`
Both

Events

  1. clickActivate
    Payload
    `{ tileId: string }`. Fires when the user activates the tile (click on `interactive` variant, Enter / Space on keyboard-focused tile). For tiles wrapping a navigation anchor, the consumer's router handles navigation and this event is informational.
    Web Components
    `clickActivate` CustomEvent on the host with `event.detail = { tileId }`.
    React
    `onClick(event)` standard DOM event; `onActivate(tileId)` when consumers prefer the canonical event shape.
    Angular Signals
    `output<string>('clickActivate')`.
    Vue
    `@click-activate` event with payload `{ tileId }`.
  2. selectedChange
    Payload
    `{ tileId: string, selected: boolean }`. Fires when the tile's selected state changes — by checkbox click, whole-tile click on selectable variant, or programmatic change. Consumers managing multi-select state aggregate these events into the selection set.
    Web Components
    `selectedChange` CustomEvent with `event.detail = { tileId, selected }`.
    React
    `onSelectedChange(selected: boolean)` per-tile callback or aggregated `onSelectionChange(set: Set<string>)` at the grid level.
    Angular Signals
    `output<{ tileId: string, selected: boolean }>('selectedChange')`.
    Vue
    `@update:selected` on the tile or `@update:selection` on the grid wrapper.
Both

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.
Both

Performance thresholds

  • lazyLoadOffscreenThresholdviewport-distance200px

    Tiles 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-count2columns

    Below `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-count6columns

    Above `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.

Both

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.

Both

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.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFor 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

TriggerExpected
Focus enters interactive tileSR 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 togglesSR 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-role
  • aria-required-attr
  • aria-required-children
  • aria-required-parent
  • color-contrast
  • image-alt
  • role-img-alt
  • link-name
  • button-name
Both

Common mistakes

#tile-as-card

Tile used for content-led layout

Problem

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.

Fix

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

Problem

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.

Fix

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

Problem

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.

Fix

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

Problem

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.

Fix

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

Problem

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.

Fix

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.