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.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.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>
);
}
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}
/>;
.route {
display: flex;
flex-direction: column;
block-size: 100%;
}
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.| Prop | Type | Default | Description |
|---|---|---|---|
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. |
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.| Prop | Type | Default | Description |
|---|---|---|---|
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. |
avatarStatusIndicator suppresses avatarBadgeSrc and avatarBadgeContent.| Prop | Type | Default | Description |
|---|---|---|---|
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. |
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
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 };
interface StatusIndicator {
type: StatusIndicatorType;
contentDescription?: string;
}
interface StatusIcon {
icon: IconSource;
accessibilityContentDescription?: string;
/** Per-icon tint. `statusIndicatorIconTintColor` still wins over it. */
tintColor?: IconTintColor;
}
const AvatarShape = { CIRCLE, ROUNDED_RECTANGLE };
const PlaceholderStyle = { AVATAR, IMAGE };
const StatusIndicatorType = {
ACTIVE, INACTIVE, NOTIFICATION, UNREAD,
POSITIVE, NEGATIVE, WARNING, CAUTION,
};
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.timestamp and the status treatment (statusIndicator and statusIndicatorIcons) — these two share the trailing slot and can appear together;showSwitch;showRadioButton;accessoryIcon;trailingTag.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.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.onCheckedChange with the inverted checked value in switch and radio mode, and onClick otherwise.onSliderValueChange fires on key release with the next clamped value. Up and Down continue to move focus.disabled.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.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.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.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.avatarAlt, with the avatarStatusIndicator label appended when one is set.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.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.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.VerticalList, directly adjacent with no added spacing.timestamp for time values so the slot remains semantically clear.ListItem props instead of rebuilding the row from containers.