ControlTile

Updated: Sep 24, 2026
ControlTile is the quick-settings tile: one icon, one short title, and an optional state — a toggle, a ring of progress around the icon, or a horizontal bar that replaces the title. It is a single focus target that owns its own material, focus and press treatment, corner shape, and accessible role.
Use it for glanceable controls arranged in a small grid, such as a settings dashboard. It is not a list row and not an app launcher. For an app, person, or destination shortcut use AppControlTile, which shows app identity and has no toggle or progress state. For a dense settings list use ListItem; for a single compact adjustable value use IsolatedControl.
ControlTile builds on Container and accepts its props, so material, shape, tooltips, and interaction hooks all work here.

Usage

A toggle tile and an adjustable tile in a responsive row:
import { useState } from 'react';
import { ControlTile } from '@wearables-ui-toolkit/mrbd';
import { brightnessIcon, wifiOffIcon, wifiOnIcon } from './icons';

export function QuickSettings() {
  const [wifiEnabled, setWifiEnabled] = useState(true);
  const [brightness, setBrightness] = useState(0.5);

  const clamp = (next: number) => Math.min(1, Math.max(0, next));

  return (
    <div className="tile-row">
      <ControlTile
        title="Wi-Fi"
        icon={wifiEnabled ? wifiOnIcon : wifiOffIcon}
        iconAnimationKey={wifiEnabled ? 'on' : 'off'}
        animateIconChanges
        checked={wifiEnabled}
        onClick={() => setWifiEnabled(value => !value)}
      />
      <ControlTile
        title="Brightness"
        icon={brightnessIcon}
        progress={brightness}
        showHorizontalProgressBar
        lockFocus
        onIncrement={current => setBrightness(clamp(current + 0.1))}
        onDecrement={current => setBrightness(clamp(current - 0.1))}
      />
    </div>
  );
}
A ring of progress around the icon, keeping the title visible:
import { ControlTile, IconTintColor } from '@wearables-ui-toolkit/mrbd';
import { downloadIcon } from './icons';

<ControlTile
  title="Downloading"
  icon={downloadIcon}
  iconTintColor={IconTintColor.ACTION}
  progress={completedFraction}
  showCircularProgressBar
  animateProgressChanges
/>;
The tile has no width of its own — it fills the space its parent gives it, so lay tiles out with flex and a spacing token instead of fixed sizes:
.tile-row {
  display: flex;
  align-items: stretch;
  gap: var(--uit-spacing-small);
}

.tile-row > * {
  flex: 1 1 0;
  min-width: 0;
}

Props

ControlTileProps extends ContainerProps without children. Read that page for the full inherited surface.

Own props

PropTypeDefaultDescription
title
string
—
Short label rendered beside the icon. Also becomes the accessible name.
titleMaxLines
number
2
Maximum number of title lines before the text is clamped.
checked
boolean \| null
null
Toggle state. null or omitted means the tile is not a toggle. Controlled: the tile never flips it.
icon
IconSource
—
Icon shown in the circular icon slot. Accepts inline vector data, an object with a uri, or a URL string.
iconAnimationKey
string \| number
—
Stable identity for the current icon. A change to this value is what triggers the swap animation.
animateIconChanges
boolean
false
Cross-fade and scale between icons when iconAnimationKey changes.
animateProgressChanges
boolean
false
Animate showing and hiding a progress bar, and the icon scale that goes with it.
iconTintColor
IconTintColor
—
Semantic tint for the glyph. Falls back to PRIMARY when unset.
checkedIconTintColor
IconTintColor
—
Tint used while checked is true. Falls back to iconTintColor.
iconContainerMaterial
ContainerMaterial \| null
—
Material for the circular icon backdrop. When omitted the tile uses its built-in overlay backdrop.
checkedIconContainerMaterial
ContainerMaterial \| null
the library’s checked-icon material
Material for the icon backdrop while checked is true. Pass null to keep the ordinary backdrop while checked.
showCircularProgressBar
boolean
false
Draw a progress ring around the icon. Requires progress.
showHorizontalProgressBar
boolean
false
Replace the title with a horizontal bar. Requires progress, and wins when both progress props are true.
progress
number
—
Progress value from 0 to 1, used by whichever progress bar is shown.
lockFocus
boolean
false
Keep directional focus on this tile and turn off partial-focus rubberband feedback.
onLockedFocusLost
() => void
—
Called when a locked tile leaves the focused or pressed state.
onIncrement
(currentValue: number) => void
—
Called with the current progress when ArrowRight is released while the horizontal bar is shown.
onDecrement
(currentValue: number) => void
—
Called with the current progress when ArrowLeft is released while the horizontal bar is shown.
onClick
() => void
—
Activation handler for pointer clicks and Enter or Space.

Notable inherited props

PropTypeDefaultDescription
material
ContainerMaterial
the tile’s own default material
Material for the tile surface. Memoize the instance.
shapeProvider
ShapeProvider
a rounded rectangle at the medium corner radius
Geometry for clipping and material paths.
disabled
boolean
false
Disables activation and arrow-key adjustment.
ariaLabel
string
derived from title
Overrides the accessible name; required for icon-only tiles.
onStateChange
(prev, next) => void
—
Observe default, focused, and pressed transitions. Still called when lockFocus is set.
visualStateOverride
VisualState
—
Force a visual state instead of deriving it from interaction. Also drives the icon backdrop and bar appearance.
partialFocusSupportedAxis
PartialFocusSupportedAxis
PartialFocusSupportedAxis.XY
Axes that get handoff and rubber-band feedback. Forced to None while lockFocus is set.
tooltipText
string
—
Tooltip shown for the tile, useful when a clamped title hides the full label.
The tile emits its own role, ariaLabel, and aria-* value attributes before spreading the remaining inherited props, so anything you pass for those wins. That is how ariaLabel replaces the name derived from title. The same applies to role and the value attributes — override those only when you really do mean different semantics than the tile inferred.

Types and enums

IconTintColor — semantic tints accepted by iconTintColor and checkedIconTintColor. Raw colour strings are deliberately not accepted.
  • PRIMARY, SECONDARY, ACCENT
  • ON_CHECKED
  • ACTION, POSITIVE, WARNING, NEGATIVE, INFO

Behavior

  • Sizing. The tile has no fixed width; it fills its parent. Its height wraps its content above a minimum, so a two-line title grows the tile. Keep tiles in a flex row and let them share the space.
  • Layout. The circular icon slot leads, and the title — or the horizontal bar that replaces it — fills the remaining width.
  • Toggle state.checked is controlled. Flip it in onClick; the tile only renders what you pass. null or omitted means the tile is a plain button.
  • Progress needs a value. Neither progress bar renders unless progress is set. showCircularProgressBar or showHorizontalProgressBar on their own change nothing visible.
  • Progress modes are mutually exclusive. If both progress props are true, the horizontal bar wins. The horizontal bar fades the title out and takes its place and the icon backdrop is suppressed; the circular ring is drawn inside the icon slot, keeps the backdrop, and leaves the title visible.
  • Bar state follows the tile. While the horizontal bar is shown it uses its focused appearance whenever the tile is focused or pressed, and its idle appearance otherwise.
  • Icon sizing. The glyph renders at the medium icon size whenever either progress prop is set — even before progress gives it something to draw — and at the large size otherwise. It also scales up slightly in the horizontal-bar layout.
  • Icon backdrop. A base circle sits behind the glyph whenever the horizontal bar is not shown. The emphasis layer on top of it appears when the tile is focused or pressed, when checked is true, or when an icon container material is supplied — and only when there is an icon and showHorizontalProgressBar is not set. A supplied iconContainerMaterial replaces the built-in overlay and is drawn pinned to its focused appearance so the backdrop reads consistently; while checked is true the checked material cross-fades over it.
  • Icon tint resolution. While checked is not true the tint is iconTintColor. While checked it is checkedIconTintColor, then iconTintColor, and finally ON_CHECKED when the default checked backdrop material is in use. With nothing set at all, the glyph falls back to PRIMARY.
  • Icon swaps.animateIconChanges cross-fades and scales the old glyph out and the new one in. It only runs when iconAnimationKey actually changes, so give each icon a stable key; without a key the icon swaps instantly.
  • Activation. Enter, Space, and pointer clicks call onClick.
  • Arrow-key adjustment. While showHorizontalProgressBar is set and the tile is not disabled, ArrowLeft and ArrowRight are consumed so they do not move focus. On key release the tile calls onDecrement or onIncrement with the current progress, and only when progress is set. Your own onKeyDown and onKeyUp handlers are not called for those keys. The tile does not change progress itself — you compute and apply the next value.
  • Locked focus.lockFocus consumes all four arrow keys so directional input stays on the tile, and it disables partial-focus rubberband feedback. Use it for continuous adjustment, give the user an obvious way out, and use onLockedFocusLost to notice when the tile stops being focused anyway.

Accessibility

  • When progress is set the tile reports role="slider" with aria-valuenow equal to progress and a range of 0 to 1. This applies whenever progress is non-null, even before a progress bar is shown, and the value is reported verbatim — keep it inside the range.
  • Otherwise it reports role="switch" with aria-checked when checked is a boolean, and role="button" when checked is null or omitted.
  • The accessible name is derived from title. An icon-only tile has no name, so pass ariaLabel explicitly.
  • While the horizontal bar is shown, the bar itself is also exposed as a slider named from title, falling back to Progress when there is no title. Give an adjustable tile a title so both readings agree.
  • A clamped title is still fully readable through tooltipText, which is a better answer than raising titleMaxLines until the grid breaks.
  • Keep the tile as one focus target. A nested focusable control creates conflicting interaction semantics.
  • Keep titles to one or two words so they fit without clamping.
  • Give each tile independent state that changes something observable.
  • Memoize any custom material instance so it is not recreated every render.
  • Provide iconAnimationKey whenever you animate an icon swap.
  • Pass progress whenever either progress prop is set so the tile reports the correct state.

Avoid

  • Place the tile directly in its layout because it already owns a rounded material surface.
  • Choose one progress presentation; relying on precedence when both progress props are set makes the result unclear.
  • Update the application-owned value from onIncrement and onDecrement. These callbacks report intent and do not move the bar themselves.
  • Derive dimensions from the layout rather than hardcoding a particular display size.
  • Keep the tile as one focus target instead of nesting another focusable element.
  • Use AppControlTile for an app shortcut; ControlTile represents a setting.