ListItem

Updated: Sep 24, 2026
ListItem is the standard row for collections: inboxes, settings, notification lists, media pickers, menus, and status views. One ListItem is one row-sized interaction target. It owns the entire row on your behalf — material, focus and press states, corner shape, row height, slot order, text clamping, the trailing control it renders, and the composed accessible name.
ListItem extends Container, so the container and interactable surface (material, tooltips, disabled, onStateChange, and the rest) is available on it. children, width, and height are removed from that surface: the row measures itself from its content and fills the width of its list.
Place rows directly adjacent in VerticalList, with no gap, margin, divider, or rounded wrapper between them. Use ListItem rather than imitating a row with Panel, StaticContainer, or a hand-rolled rounded rectangle; this preserves row sizing, focus, and semantics.

Usage

import { useState } from 'react';
import {
  ListItem,
  TrailingTag,
  VerticalList,
} from '@wearables-ui-toolkit/mrbd';

function InboxRoute() {
  const [notificationsOn, setNotificationsOn] = useState(true);
  const [volume, setVolume] = useState(0.4);

  return (
    <VerticalList ariaLabel="Inbox">
      <ListItem
        title="Alex Morgan"
        subtitle="Shared a photo"
        timestamp="2m"
        avatarSrc={alex.photoUrl}
        avatarAlt="Alex Morgan"
        onClick={openConversation}
      />
      <ListItem
        title="Notifications"
        subtitle="Announcements and activity"
        icon={bellIcon}
        showSwitch
        checked={notificationsOn}
        onCheckedChange={setNotificationsOn}
      />
      <ListItem
        title="Live captions"
        subtitle="Early access"
        icon={captionsIcon}
        trailingTag={TrailingTag.BETA}
        onClick={openCaptions}
      />
      <ListItem
        showSlider
        icon={volumeIcon}
        ariaLabel="Volume"
        sliderValue={volume}
        onSliderValueChange={setVolume}
      />
    </VerticalList>
  );
}
A status row puts the timestamp on the subtitle line and moves the state into the trailing status slot:
import {
  ListItem,
  StatusIndicatorType,
  SubtitleTextColor,
  TimestampPosition,
} from '@wearables-ui-toolkit/mrbd';

<ListItem
  title="Studio backup"
  subtitle="Paused"
  subtitleTextColor={SubtitleTextColor.WARNING}
  timestamp="Yesterday"
  timestampPosition={TimestampPosition.SUBTITLE}
  icon={backupIcon}
  statusIndicator={{
    type: StatusIndicatorType.WARNING,
    contentDescription: 'Needs attention',
  }}
  statusIndicatorIcons={[
    { icon: mutedIcon, accessibilityContentDescription: 'Notifications off' },
  ]}
  onClick={openBackupSettings}
/>;
The list stays edge to edge and its row container supplies the horizontal inset, so the route shell only needs to fill the available viewport:
.route {
  display: flex;
  flex-direction: column;
  block-size: 100%;
}

Props

The full prop type is ListItemProps. It is generic over the rendered element, so ListItemProps<'a'> describes a row rendered as an anchor. ListItem is polymorphic: pass as to change that element. Everything Container and InteractableBase accept is forwarded except children, width, and height.

Text

PropTypeDefaultDescription
title
string
—
Primary row text. Keep it to one identity noun phrase.
titleContentDescription
string
the title value
Spoken replacement for the title in the composed accessible name.
subtitle
string
—
One supporting line beneath the title.
subtitleContentDescription
string
the subtitle value
Spoken replacement for the subtitle.
subtitleTextColor
SubtitleTextColor
SubtitleTextColor.SECONDARY
Semantic subtitle color.
subtitleMaxLines
number
1
Subtitle line clamp. Any value above 1 switches the row to its multi-line layout, which top-aligns the row content.
timestamp
string
—
Short time or date.
timestampContentDescription
string
the timestamp value
Spoken replacement for the timestamp.
timestampPosition
TimestampPosition
TimestampPosition.ACCESSORY
Where the timestamp renders.
timestampTextColor
TimestampTextColor
PRIMARY in the accessory positions, SECONDARY on the subtitle line
Semantic timestamp color.
contentClassName
string
—
Class applied to the row’s content layout element.

Leading identity

Setting avatarSrc, avatarPrimaryContent, avatarSecondarySrc, avatarSecondaryContent, avatarBadgeSrc, or avatarBadgeContent puts the row in avatar mode; icon is then ignored. Icon and avatar are alternative identity modes, never both.
PropTypeDefaultDescription
icon
IconSource
—
Leading icon. The row owns its size.
iconTintColor
IconTintColor
IconTintColor.PRIMARY
Semantic tint for the leading icon.
avatarSrc
string
—
Primary avatar image URL.
avatarPrimaryContent
ReactNode
—
Content for the avatar’s primary surface. Overrides avatarSrc.
avatarSecondarySrc
string
—
Second image for the duo avatar layout.
avatarSecondaryContent
ReactNode
—
Content for the avatar’s secondary surface. Overrides avatarSecondarySrc.
avatarBadgeSrc
string
—
Image for the avatar badge slot.
avatarBadgeContent
ReactNode
—
Content for the avatar badge slot.
avatarAlt
string
'Avatar'
Alt text for the avatar image.
avatarShape
AvatarShape
AvatarShape.CIRCLE
Shape of the primary avatar image.
avatarStatusIndicator
StatusIndicatorType
—
Status dot drawn on the leading avatar.
avatarStatusIndicatorIcon
IconSource
—
Glyph drawn inside that status dot.
avatarPlaceholderStyle
PlaceholderStyle
—
Placeholder treatment while the avatar has no image.
secondaryIcon
IconSource
—
Small icon rendered before the subtitle, for a different supporting fact. It only appears when the second line does, so the row also needs a subtitle or a subtitle-position timestamp.
secondaryIconTintColor
IconTintColor
IconTintColor.SECONDARY
Semantic tint for the secondary icon.
The row renders the avatar at the medium avatar size; it is not configurable per row. The status dot and the badge share one slot on that avatar, so setting avatarStatusIndicator suppresses avatarBadgeSrc and avatarBadgeContent.

Trailing treatment

PropTypeDefaultDescription
showSwitch
boolean
false
Render a switch at the trailing edge and give the row switch semantics.
showRadioButton
boolean
false
Render a radio control at the trailing edge and give the row radio semantics. Ignored when showSwitch is set.
checked
boolean
false
Checked state for the switch or radio.
onCheckedChange
(isChecked: boolean) => void
—
Called with the next checked value when the row activates in switch or radio mode.
statusIndicator
StatusIndicator
—
Trailing status dot with an optional spoken description.
statusIndicatorIcons
StatusIcon[]
—
Trailing status icons, rendered before the dot.
statusIndicatorIconTintColor
IconTintColor
each icon’s own tintColor, otherwise SECONDARY
Group tint override for all status icons.
accessoryIcon
IconSource
—
Trailing accessory icon.
accessoryIconAlwaysVisible
boolean
true
When false, the accessory icon is transparent in the resting state and fades in on focus or press.
accessoryIconTintColor
IconTintColor
the primary icon color
Semantic tint for the accessory icon.
trailingTag
TrailingTag
TrailingTag.NONE
Design-system-owned tag rendered top-aligned at the trailing edge. The label is not caller-supplied.

Slider mode

PropTypeDefaultDescription
showSlider
boolean
false
Replace the row’s text layout with a full-width adjustable value.
sliderMinimumValue
number
0
Lower bound.
sliderMaximumValue
number
1
Upper bound.
sliderValue
number
0
Current value, clamped to the bounds for reporting.
sliderIncrementPercentage
number
0.1
Step per arrow press, as a fraction of the range.
onSliderValueChange
(newValue: number) => void
—
Called with the next clamped value. The row is controlled: update sliderValue from this callback.

Notable inherited props

PropTypeDefaultDescription
onClick
MouseEventHandler
—
Row activation. Not called while a switch or radio is shown; onCheckedChange fires instead.
disabled
boolean
false
Disables activation, key handling, and the checked or slider callbacks.
material
ContainerMaterial
the row material
Replaces the row material. The row always re-applies its own material inset.
shapeProvider
ShapeProvider
rounded rectangle at CornerRadius.MEDIUM
Row geometry for clipping and material paths.
ariaLabel / aria-label
string
—
Prefix prepended to the composed accessible name rather than replacing it.
partialFocusSupportedAxis
PartialFocusSupportedAxis
Y in slider mode, otherwise the container default
Axes that produce partial-focus feedback.
role
string
derived from the row’s mode
Escape hatch when you render the row as a non-div element.
as
ElementType
'div'
Element the row renders.
onStateChange
(prev: InteractionState, next: InteractionState) => void
—
Interaction-state notifications. Called in addition to the row’s own state tracking.

Types and enums

const SubtitleTextColor = {
  PRIMARY, SECONDARY, POSITIVE, WARNING, NEGATIVE, INFO,
};

const TimestampPosition = {
  ACCESSORY,      // trailing slot, vertically centered (default)
  ACCESSORY_TOP,  // trailing slot, top aligned
  SUBTITLE,       // inline on the subtitle line
};

const TimestampTextColor = { PRIMARY, SECONDARY, ACCENT };

const IconTintColor = {
  PRIMARY, SECONDARY, ACCENT, ON_CHECKED,
  ACTION, POSITIVE, WARNING, NEGATIVE, INFO,
};

const TrailingTag = { NONE, BETA };
Two object shapes describe the status treatment:
interface StatusIndicator {
  type: StatusIndicatorType;
  contentDescription?: string;
}

interface StatusIcon {
  icon: IconSource;
  accessibilityContentDescription?: string;
  /** Per-icon tint. `statusIndicatorIconTintColor` still wins over it. */
  tintColor?: IconTintColor;
}
AvatarShape, PlaceholderStyle, and StatusIndicatorType come from Avatar:
const AvatarShape = { CIRCLE, ROUNDED_RECTANGLE };
const PlaceholderStyle = { AVATAR, IMAGE };
const StatusIndicatorType = {
  ACTIVE, INACTIVE, NOTIFICATION, UNREAD,
  POSITIVE, NEGATIVE, WARNING, CAUTION,
};

Resolving a semantic color yourself

Every semantic color case has an exported pair of helpers that resolve it exactly the way the row does. Use them when your own content next to a row has to match:
import {
  TimestampTextColor,
  getTimestampTextBlendMode,
  getTimestampTextCSSVariable,
} from '@wearables-ui-toolkit/mrbd';

const timestampStyle = {
  color: getTimestampTextCSSVariable(TimestampTextColor.SECONDARY),
  mixBlendMode: getTimestampTextBlendMode(TimestampTextColor.SECONDARY),
};
getTimestampTextCSSVariable(color) and getIconTintCSSVariable(tint) return the CSS custom property reference the case maps to. getTimestampTextBlendMode(color) and getIconTintBlendMode(tint) return 'lighten' for the SECONDARY case and undefined for every other case, which is what keeps a dimmed glyph legible on the additive-light display.

Behavior

One trailing treatment per row

The row resolves a single trailing treatment. Higher entries suppress every lower entry, so extra props are silently dropped rather than stacked:
  1. timestamp and the status treatment (statusIndicator and statusIndicatorIcons) — these two share the trailing slot and can appear together;
  2. showSwitch;
  3. showRadioButton;
  4. accessoryIcon;
  5. trailingTag.
A subtitle-position timestamp still counts as the row’s timestamp, so it also suppresses the switch, radio, accessory icon, and tag. Pick the treatment that carries the row’s meaning and let the rest go.

Timestamp placement

ACCESSORY and ACCESSORY_TOP render the timestamp in the trailing slot, centered or top-aligned respectively, and default to the primary text color. SUBTITLE renders it on the subtitle line, separated from the subtitle by a bullet when both are present, and defaults to the secondary text color.
Reserve the slot for a short time or date. A location, category, count, or status string put there steals width from the row’s identity and misrepresents the information.

Slider rows

showSlider drops title and subtitle and gives the row a full-width value bar plus the optional leading icon. The trailing slot is not drawn at all, so a timestamp, status treatment, switch, radio, accessory icon, or tag never appears on a slider row — do not combine them. The row reports slider semantics, and partial-focus feedback is restricted to the vertical axis so horizontal input stays with the value.

Keyboard

  • Activation (Enter or Space, or a pointer click) calls onCheckedChange with the inverted checked value in switch and radio mode, and onClick otherwise.
  • In slider mode, Left and Right are captured before focus navigation sees them, and onSliderValueChange fires on key release with the next clamped value. Up and Down continue to move focus.
  • All key handling is skipped while disabled.

Accessory icon visibility

With accessoryIconAlwaysVisible set to false, the accessory icon is fully transparent in the resting state and fades in when the row is focused or pressed. It still occupies its slot, so row text does not reflow.

Material and sizing

The row applies its own material inset over whatever material you pass, clips to a rounded rectangle at the medium corner radius, and derives its height from its content plus a minimum row height. Multi-line rows top-align the content instead of centering it. Do not set a width or height on the row, and do not resize the component-owned icon, avatar, or status slots.

Color and the additive-light display

Secondary foregrounds — the default subtitle, the secondary icon, the subtitle-line timestamp, and status icons — composite with a lighten blend so dim glyphs lift against the material behind them. That is why the color props take semantic cases (SubtitleTextColor, TimestampTextColor, IconTintColor) instead of CSS color strings: the component resolves the case to a token and pairs it with the correct blend. The status dot is painted from the --uit-color-persistent-* role that matches its StatusIndicatorType.

Accessibility

The row composes one accessible name from, in order: your ariaLabel or aria-label prefix, the title, the subtitle, the timestamp (only when it is actually rendered), the statusIndicator description, the trailing tag label, and each StatusIcon description. The content-description props replace the visible text in that string without changing what is drawn — use them when abbreviated copy would be ambiguous when spoken.
The role follows the treatment the row actually renders: slider in slider mode, switch for a rendered switch, radio for a rendered radio, and button otherwise — so a switch suppressed by a timestamp or status treatment leaves the row a button. The row publishes aria-checked for a switch or radio, and the aria-valuemin, aria-valuemax, and aria-valuenow triple for a slider. aria-valuenow reports the clamped value, so a sliderValue outside the bounds is announced at the bound rather than out of range. The embedded switch, radio, and value bar are presentational, which leaves the row as the single focus stop and the only node reporting the control’s state.
The leading avatar is the one exception: it reports itself as an image named by avatarAlt, with the avatarStatusIndicator label appended when one is set.
The derived role is applied when the row renders as a plain div, and always when it is a switch, radio, or slider. Render the row as another element — an anchor or a router link, say — and an ordinary row keeps that element’s own implicit semantics instead of being relabelled a button. Pass role to override the derived value outright.
Any aria-* attribute other than aria-label wins over the value the row computes. Use that to add aria-valuetext so a slider value is spoken as words, or to report an aria-valuenow other than the clamped one. aria-label is the exception: it is folded into the composed name as a prefix instead of replacing it.
An ariaLabel is required for rows with no title, notably slider rows. Identity avatars require avatarAlt; a trailing statusIndicator requires contentDescription; and any meaningful status icon requires accessibilityContentDescription. avatarAlt and the built-in status fallback are untranslated English strings, so applications must provide localized values.
  • Put every row in one VerticalList, directly adjacent with no added spacing.
  • Give each fact one home rather than repeating a time or status across the subtitle, timestamp, tag, and status slots.
  • Use the row’s timestamp, status, accessory, and control props so alignment and reserved space remain consistent across the collection.
  • Let the row own interaction when it shows a switch, radio, or slider.
  • Validate realistic long values. Systematic ellipsis across a collection usually indicates a content-model problem rather than a layout problem.

Avoid

  • Use descriptive text rather than adding a chevron or arrow accessory merely to imply navigation.
  • Choose one trailing treatment because only one survives precedence.
  • Use non-focusable content for static summary cards, legends, and section headings; a focusable row implies an action.
  • Keep timestamp for time values so the slot remains semantically clear.
  • Row internals and generated class names are not supported customization surfaces. Use ListItem props instead of rebuilding the row from containers.