# Picture tile

A pressable tile with a picture and a short label under it, for choosing among looks, presets or templates in a gallery.

Block: `smbk-picture-tile`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `ratio` | `4-3`, `1-1` | `4-3` | The picture's proportion, width to height: 4:3 or square. |
| `selected` | on / off | off | The chosen tile: the rim edge at the emphasis width and a tick at the picture's corner. Pressed for assistive technology. |

Parts: `smbk-picture-tile__picture`, `smbk-picture-tile__label`, `smbk-picture-tile__tick`

## Markup

```html
<button type="button" class="smbk-picture-tile smbk-picture-tile--ratio-4-3" aria-pressed="false">
  <span class="smbk-picture-tile__picture"><img src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22480%22%20height%3D%22270%22%3E%3Crect%20width%3D%22480%22%20height%3D%22270%22%20fill%3D%22%235c7798%22%2F%3E%3Ccircle%20cx%3D%22360%22%20cy%3D%2290%22%20r%3D%2260%22%20fill%3D%22%23cc0d0d%22%2F%3E%3C%2Fsvg%3E" alt=""/></span>
  <span class="smbk-picture-tile__label">Halftone</span>
</button>
```

## React

```tsx
import { PictureTile } from './design-system/components/picture-tile/picture-tile.js';

<PictureTile ratio="4-3" label="Halftone" src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22480%22%20height%3D%22270%22%3E%3Crect%20width%3D%22480%22%20height%3D%22270%22%20fill%3D%22%235c7798%22%2F%3E%3Ccircle%20cx%3D%22360%22%20cy%3D%2290%22%20r%3D%2260%22%20fill%3D%22%23cc0d0d%22%2F%3E%3C%2Fsvg%3E" />
```

## Stylesheet

```css
/* smbk-picture-tile — Picture tile. Figma "Picture tile" 7119:143 (Selected × State × Ratio, 20 variants).
 * One native button: a picture on top, at 4:3 or 1:1, and a short label under
 * it, in a frame of the default surface with a hairline edge in the subtle
 * border color and a small container's corner. It fills the cell it is given,
 * never narrower than the 88 size step, so a grid of them wraps. Pointing
 * strengthens the edge; pressing sinks the frame onto the sunken surface;
 * focus draws the shared ring outside it. Chosen, the edge turns the rim color
 * at the emphasis width, drawn inside so nothing moves, the tile lifts with
 * the control elevation, and a tick on a brand disc marks the picture's
 * corner: the shape says it is chosen, not the color alone. Disabled dims the
 * picture and the label. */

.smbk-picture-tile {
  box-sizing: border-box;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-sm);
  width: 100%;
  min-width: var(--smbk-size-88);
  margin: 0;
  padding: var(--smbk-space-inset-2xs);
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
  border-radius: var(--smbk-radius-container-sm);
  background: var(--smbk-color-surface-default);
  color: var(--smbk-color-text-default);
  font: inherit;
  text-align: start;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* The picture: the tile's width at the ratio, on the sunken surface, with the inset corner. Whatever it holds
   (a picture, an image placeholder, a skeleton) fills it. */
.smbk-picture-tile__picture {
  display: block;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  border-radius: var(--smbk-radius-inset);
  background: var(--smbk-color-surface-sunken);
}

.smbk-picture-tile__picture > * {
  display: block;
  width: 100%;
  height: 100%;
}

.smbk-picture-tile__picture > img {
  object-fit: cover;
}

.smbk-picture-tile--ratio-1-1 .smbk-picture-tile__picture {
  aspect-ratio: 1 / 1;
}

/* The label: one line of Body SM, cut short with an ellipsis; the button's name stays the whole label. */
.smbk-picture-tile__label {
  display: block;
  overflow: hidden;
  padding: var(--smbk-space-inset-xs) var(--smbk-space-inset-md) var(--smbk-space-inset-md);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The tick: a brand disc with a check, 8 in from the tile's top and end corner. Shown only when chosen. */
.smbk-picture-tile__tick {
  position: absolute;
  inset-block-start: var(--smbk-space-inset-md);
  inset-inline-end: var(--smbk-space-inset-md);
  display: none;
  align-items: center;
  justify-content: center;
  width: var(--smbk-size-20);
  height: var(--smbk-size-20);
  border-radius: var(--smbk-radius-full);
  background: var(--smbk-color-action-brand-background);
  color: var(--smbk-color-icon-on-brand);
}

.smbk-picture-tile__tick .smbk-icon {
  font-size: var(--smbk-size-icon-xs);
}

/* ---- states ---- */

@media (hover: hover) {
  .smbk-picture-tile:hover:enabled:not(.smbk-picture-tile--selected) {
    border-color: var(--smbk-color-border-strong);
  }
}

.smbk-picture-tile:active:enabled {
  background: var(--smbk-color-surface-sunken);
}

.smbk-picture-tile:focus-visible {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: var(--smbk-focus-ring-offset);
}

/* Chosen: the rim at the emphasis width, the hairline border plus the rest drawn inside, and the lift. */
.smbk-picture-tile--selected {
  border-color: var(--smbk-color-border-rim);
  box-shadow: inset 0 0 0 calc(var(--smbk-border-width-emphasis) - var(--smbk-border-width-hairline)) var(--smbk-color-border-rim), var(--smbk-elevation-control);
}

.smbk-picture-tile--selected .smbk-picture-tile__tick {
  display: flex;
}

.smbk-picture-tile:disabled {
  cursor: default;
}

.smbk-picture-tile:disabled .smbk-picture-tile__picture {
  opacity: var(--smbk-opacity-disabled);
}

.smbk-picture-tile:disabled .smbk-picture-tile__label {
  color: var(--smbk-color-text-disabled);
}

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
   user's own colors. Nothing here applies outside that mode. */

@media (forced-colors: active) {
  /* The chosen edge's inner half is a shadow and would vanish: the edge takes Highlight at full width. The
     tick's disc is a fill: it keeps its shape in Highlight with the check in the highlight's text color. */
  .smbk-picture-tile--selected {
    border-width: var(--smbk-border-width-emphasis);
    border-color: Highlight;
    padding: calc(var(--smbk-space-inset-2xs) - var(--smbk-border-width-emphasis) + var(--smbk-border-width-hairline));
  }

  .smbk-picture-tile__tick {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }

  .smbk-picture-tile:disabled {
    border-color: GrayText;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "stack": {
      "sm": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things stacked one above another, small."
      }
    },
    "inset": {
      "2xs": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Padding inside a component, smallest."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Padding inside a component, extra small."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      }
    }
  },
  "size": {
    "20": {
      "$type": "dimension",
      "$value": "20px",
      "$description": "Raw size step, 20 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    },
    "88": {
      "$type": "dimension",
      "$value": "88px",
      "$description": "Raw size step, 88 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    },
    "icon": {
      "xs": {
        "$type": "dimension",
        "$value": "{{size.12}}",
        "$description": "Icon box, extra small."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    },
    "emphasis": {
      "$type": "dimension",
      "$value": "{{border-width.2}}",
      "$description": "Two-pixel edges for emphasis: a chosen card, a focus ring."
    }
  },
  "color": {
    "border": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      },
      "strong": {
        "$type": "color",
        "$value": "{{color.grey.400}}",
        "$description": "Stronger edges and separators."
      },
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      },
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      }
    },
    "surface": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.100}}",
        "$description": "Standard surface: cards, panels, the header."
      },
      "sunken": {
        "$type": "color",
        "$value": "{{color.alpha.grey-800.5}}",
        "$description": "Recessed wells: fields, tracks, code. See-through, so a well always shows one step below what it sits on: the page, a card or a panel."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      }
    },
    "action": {
      "brand": {
        "background": {
          "$type": "color",
          "$value": "{{color.red.600}}",
          "$description": "Brand button and chip: fill at rest."
        }
      }
    },
    "icon": {
      "on-brand": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "Icons on a brand-red fill."
      }
    }
  },
  "radius": {
    "container": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of cards, panels and dialogs, small."
      }
    },
    "inset": {
      "$type": "dimension",
      "$value": "{{radius.2}}",
      "$description": "Small corner inside a control: a checkbox mark, a track."
    },
    "full": {
      "$type": "dimension",
      "$value": "{{radius.9999}}",
      "$description": "Capsule: pills, chips, switches."
    }
  },
  "typography": {
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  },
  "elevation": {
    "control": {
      "$type": "shadow",
      "$value": {
        "light": "0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430d",
        "dark": "0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a"
      }
    }
  },
  "opacity": {
    "disabled": {
      "$type": "number",
      "$value": 0.5,
      "$description": "Fade for disabled pictures and media (50%)."
    }
  }
}
```

## Purpose

Lets someone choose among things best told apart by a picture: a look, a preset, a template, a theme. Each tile is one button with a picture on top and a short label under it, laid out in a grid that wraps. Pointing at a tile can preview it; pressing it chooses it.

## Anatomy

- The block, one button: a frame on the default surface with a thin edge and a small rounded corner, as wide as the cell it is given.
- The picture part, at a 4:3 or square proportion on the sunken surface. It holds the picture, an image placeholder while there is none, or a skeleton while it loads.
- The label part, one line of small body text under the picture, cut short with an ellipsis when it is too long.
- The tick part, shown on the chosen tile only: a check on a brand-colored disc at the picture's top corner.

## When to use

- A gallery of options that look different, where the picture says more than the name: filters, styles, layouts, templates.
- With one tile chosen, as a set of toggles where one is on.

## When not to use

- For an article or a product with a title, a description and actions. Use a [card](card.md).
- For options that differ only by name. Use [radios](radio.md), a [segmented control](segmented-control.md) or a [select](select.md).
- For a picture that opens a larger view and nothing else. Use a link around the picture.

## Behavior

The tile fills the cell it is given and is never narrower than a small minimum, so a grid of tiles wraps on a narrow screen. Pointing at a tile strengthens its edge; pressing sinks it onto the sunken surface; keyboard focus draws the focus ring around it. The chosen tile shows a stronger edge at double width, a slight lift and the tick; pointing at it changes nothing more. A disabled tile dims its picture and label and cannot be pressed. A picture that fails to load gives way to the image placeholder, named by the picture's alternative text when it has one, so the tile never shows the browser's broken-picture mark.

## Accessibility

### What it does for you

- The whole tile is one native button and one Tab stop, named by its label.
- With `selected` set, true or false, the tile is a toggle and a screen reader says whether it is pressed.
- The picture is decoration with an empty text alternative, since the label names the tile.
- The tick, not only the edge's color, shows which tile is chosen. In high contrast mode the chosen edge and the tick take the highlight color.
- The label is cut short with an ellipsis when long, but the button's name is the whole label.
- A picture that fails to load gives way to the image placeholder, named by the picture's alternative text when it has one.

### What you must do

- Leave `selected` out for a tile that does something once when pressed.
- Give the picture a text alternative when it shows something the label does not say.
- Preview anything a tile previews on pointing also on keyboard focus: nothing may be shown by pointing alone.
- Keep labels short so everyone sees all of them.

### How to test

- Tab to a tile: the focus ring shows around it, in both themes and in a forced-color mode.
- Press Enter, then Space: each presses the tile. A disabled tile cannot be pressed.
- With a screen reader, a tile is read as a button by its whole label. A selecting tile is read as pressed or not pressed.
- With text at 200% and the page 320 pixels wide, the tiles wrap into fewer columns and each keeps its label.
- In a forced-color mode, the chosen tile's edge and tick are drawn in the highlight color.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the tile. A disabled tile is skipped. |
| Enter or Space | Presses the tile. |

## Usage rules

### Do

- Keep labels to a word or two.
- Show the same subject in every tile, so only the option changes.
- Give each tile the same proportion in one gallery.

### Don't

- Put a button or a link inside a tile.
- Mix tiles that choose with tiles that run an action in one gallery.
