Dots

Choice Tile

Large pressable tiles for choosing one or several options, each with a title, description, art and price.

Transfer speed
"use client"

import * as React from "react"

The ChoiceTile component turns each option of a radio or checkbox group into a whole pressable tile with a title, an optional description, leading art and a trailing value such as a price. Use it when an option needs more than a word: transfer speed with a fee, a plan with a price, a recipient with a handle. Pick RadioGroup or Checkbox for plain form options, and ChipGroup for short options in one row.

Installation

pnpm dlx shadcn@latest add @dots/choice-tile

Usage

import { ChoiceTile, ChoiceTileGroup } from "@/components/ui/choice-tile"
<ChoiceTileGroup label="Transfer speed" defaultValue="standard">
  <ChoiceTile value="standard" title="Standard" description="1–3 business days" trailing="Free" />
  <ChoiceTile value="instant" title="Instant" description="In a few seconds" trailing="$3.00" />
</ChoiceTileGroup>

Composition

Single choice:

ChoiceTileGroup
└── ChoiceTile

Multiple choice:

ChoiceTileCheckboxGroup
└── ChoiceTileCheckbox

A ChoiceTileCheckbox can also stand alone as a single toggle.

Features

  • Single choice on React Aria RadioGroup, multiple choice on CheckboxGroup
  • Two looks: fill inverts the selected tile, indicator adds a round or square control at the end
  • Title, badge, description, leading art, a trailing value and extra content under the text
  • Rows, or a grid of stacked tiles with 1–4 columns per breakpoint
  • Group label, description and error message, with validation from React Aria
  • A nested surface for tiles inside a card, sheet or dialog
  • Look props set on the group apply to every tile unless a tile overrides them

Indicator

variant="fill" (the default) suits short options. Use variant="indicator" for richer options with a description or extra details: a round control sits at the end and the selected tile only steps its fill up. Children render as extra content under the text row.

"use client"

import { ChoiceTile, ChoiceTileGroup } from "@/components/ui/choice-tile"

Badge

Pass a Badge with variant="raised" to badge to call out a saving or a recommended option. On a selected fill tile, neutral badges invert to stay readable.

"use client"

import { Badge } from "@/components/ui/badge"

Leading

Use leading for an avatar, flag, logo or icon before the text.

Send to
"use client"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"

Stack

Set layout="stack" to stand tiles in a grid. The default is two columns.

Top-up amount
"use client"

import { ChoiceTile, ChoiceTileGroup } from "@/components/ui/choice-tile"

Columns

Set columns to a count from 1 to 4, or to { base, sm, lg } to change the count from the sm and lg breakpoints up.

"use client"

import { ChoiceTile, ChoiceTileGroup } from "@/components/ui/choice-tile"

Multiple

Use ChoiceTileCheckboxGroup with ChoiceTileCheckbox for add-ons or services where several can be on. Checkbox tiles default to the indicator variant with a square check.

Add-ons

1 selected

"use client"

import * as React from "react"

Nested

Set surface="nested" when the group sits inside a card, sheet or dialog.

"use client"

import { Card, CardContent } from "@/components/ui/card"

Invalid

Set isInvalid (or isRequired with form validation) and an errorMessage. To bring people back to the group, focus its first enabled input through ref.

Why are you disputing this payment?
"use client"

import * as React from "react"
groupRef.current
  ?.querySelector<HTMLInputElement>("input:not(:disabled)")
  ?.focus()

Disabled

Set isDisabled on a tile to rule out one option and say why in its description, or on the group to disable every tile.

"use client"

import { ChoiceTile, ChoiceTileGroup } from "@/components/ui/choice-tile"

Accessibility

Each tile is a React Aria Radio or Checkbox: the tile is the <label> of a real, visually hidden input. React Aria handles the group role, arrow keys between radio tiles, Space to toggle, required and invalid states, and the link between the error message and the group.

Name the group

Give the group a visible label, or an aria-label when there is none. A group description is read with every tile.

What is read for each tile

title and trailing form the tile's accessible name, so "Instant, $3.00" is read as one option. badge, description and extra content are kept out of the name and linked as the description instead. Keep the title short and put details in the description.

Meaning beyond color

The fill variant shows the selection by color alone. Prefer variant="indicator" when tiles sit on a background with low contrast, and keep the reason for a disabled tile in its description.

API Reference

ChoiceTileGroup

Single choice between tiles. Accepts all React Aria RadioGroup props.

PropTypeDefaultDescription
valuestring | null-The controlled selected value.
defaultValuestring | null-The initial value when uncontrolled.
onChange(value: string) => void-Called when the selection changes.
labelReact.ReactNode-Visible label of the group.
descriptionReact.ReactNode-One line under the label, read with every tile.
errorMessageReact.ReactNode-Shown under the tiles while invalid.
isInvalidbooleanfalseMarks the group invalid.
isRequiredbooleanfalseRequires a choice.
isDisabledbooleanfalseDisables every tile.
variant"fill" | "indicator""fill"Look of every tile.
layout"row" | "stack""row"Tiles in rows, or stacked in a grid.
columns1 | 2 | 3 | 4 | { base, sm?, lg? }2Grid columns when layout="stack".
surface"default" | "nested""default""nested" inside a card, sheet or dialog.
refReact.Ref<HTMLDivElement>-The group element, e.g. to focus a tile after validation.
classNamestring-Additional classes to apply to the group.

ChoiceTile

One option of a ChoiceTileGroup. Accepts all React Aria Radio props.

PropTypeDefaultDescription
valuestring-The value reported when this tile is selected.
titleReact.ReactNode-Required. The option itself. Part of the accessible name.
badgeReact.ReactNode-A Badge next to the title. Read as part of the description.
descriptionReact.ReactNode-One quiet line under the title.
leadingReact.ReactNode-Avatar, flag, logo or icon before the text.
trailingReact.ReactNode-Price or value at the end. Part of the accessible name.
childrenReact.ReactNode-Extra non-interactive content under the text row.
isDisabledbooleanfalseDisables this tile.
variant"fill" | "indicator"from group, "fill"Overrides the group look.
layout"row" | "stack"from group, "row"Overrides the group layout.
surface"default" | "nested"from group, "default"Overrides the group surface.
classNamestring-Additional classes to apply to the tile.

ChoiceTileCheckboxGroup

Multiple choice between tiles. Accepts all React Aria CheckboxGroup props, and the same label, description, errorMessage, variant, layout, columns, surface, ref and className props as ChoiceTileGroup.

PropTypeDefaultDescription
valuestring[]-The controlled selected values.
defaultValuestring[]-The initial values when uncontrolled.
onChange(value: string[]) => void-Called when the selection changes.

ChoiceTileCheckbox

A tile that toggles on its own or as one option of a ChoiceTileCheckboxGroup. Accepts all React Aria Checkbox props and the same content and look props as ChoiceTile.

PropTypeDefaultDescription
valuestring-The value reported inside a group.
isSelectedboolean-The controlled state when used on its own.
defaultSelectedboolean-The initial state when used on its own.
onChange(isSelected: boolean) => void-Called when the state changes.
variant"fill" | "indicator"from group, "indicator""indicator" shows a square check.