Modal

Updated: Sep 24, 2026
Modal is a panel-shaped content surface for a single, self-contained message: a title, optional supporting copy or a short list of points, one piece of leading media, and a row of actions.
It is a layout component, not an overlay. It renders inline wherever you place it, draws no backdrop, does not trap focus, does not claim dialog semantics, and never dismisses itself. The route or container that shows the modal owns presentation and dismissal.
Let the component own its panel material and rounded shape, mutually exclusive leading area, title and subtitle typography, spacing, list rows, actions slot, combined accessible name, and long-subtitle overflow affordance. This keeps the message layout and accessibility behavior coordinated.

Usage

A confirmation with a leading icon and two actions:
import { Button, ButtonGroup, Modal, Page } from '@wearables-ui-toolkit/mrbd';

export function PermissionScreen({ onAllow, onDismiss }: PermissionScreenProps) {
  return (
    <Page>
      <Modal
        title="Allow notifications?"
        subtitle="Notifications keep playback controls available while you are in another app."
        icon={notificationsIcon}
        buttons={
          <ButtonGroup>
            <Button title="Not now" onActivate={onDismiss} />
            <Button title="Allow" onActivate={onAllow} />
          </ButtonGroup>
        }
      />
    </Page>
  );
}
A banner with a tag, in the taller proportion:
import {
  Button,
  ButtonGroup,
  Modal,
  ModalBannerSize,
  ModalBannerTag,
} from '@wearables-ui-toolkit/mrbd';

<Modal
  title="Live translation"
  subtitle="Translate what you hear, in the moment."
  bannerSrc="/media/translation-banner.webp"
  bannerAlt="Two people talking with translated captions between them"
  bannerSize={ModalBannerSize.TALLER}
  bannerTag={ModalBannerTag.BETA}
  buttons={
    <ButtonGroup>
      <Button title="Try it" onActivate={startTranslation} />
    </ButtonGroup>
  }
/>;
A list of points instead of a subtitle. listItems replaces subtitle, so pass one or the other:
import { IconTintColor, Modal, type ModalListItem } from '@wearables-ui-toolkit/mrbd';

const capabilities: ModalListItem[] = [
  { icon: cameraIcon, description: 'Capture photos and video' },
  { icon: micIcon, description: 'Record audio' },
  { icon: lockIcon, description: 'Stored on device', iconColor: IconTintColor.POSITIVE },
];

<Modal title="What this app can do" listItems={capabilities} />;
A compact, title-only variant for a single line of guidance:
import { Modal, ModalContentMode } from '@wearables-ui-toolkit/mrbd';

<Modal title="Hold to talk" contentMode={ModalContentMode.TITLE_ONLY} />;
As a carousel item, where focus drives selection:
import { Carousel, Modal } from '@wearables-ui-toolkit/mrbd';

<Carousel onItemCentered={setStep}>
  <Modal isCarouselItem title="Look up" subtitle="Wake the display with a glance." />
  <Modal isCarouselItem title="Swipe" subtitle="Move between cards with the trackpad." />
</Carousel>;

Props

The full prop type is ModalProps. It extends the non-interactive Panel surface, minus children, clickable, focusable, onClick, and pressable — the modal manages its own focus and click behavior and takes its content through the slots below.

Own props

PropTypeDefaultDescription
title
string
—
Primary line. Hidden when omitted or empty.
subtitle
string
—
Supporting copy. Hidden in title-only mode and whenever listItems are supplied.
contentMode
ModalContentMode
ModalContentMode.STANDARD
Selects the standard presentation or the compact title-only presentation.
icon
IconSource
—
Leading icon. Lowest priority of the four leading treatments.
bannerSrc
string
—
Banner image URL. Highest priority of the four leading treatments.
bannerAlt
string
'Banner image'
Alternative text passed to the banner image fallback. The complete leading area is decorative and hidden from assistive technology.
bannerSize
ModalBannerSize
ModalBannerSize.STANDARD
Banner aspect ratio.
bannerTag
ModalBannerTag
ModalBannerTag.NONE
Tag overlaid on the bottom of the banner. Ignored without a banner.
logoSrc
string
—
Logo image URL, rendered in a circular Avatar at the large size.
logoAlt
string
'Logo'
Alternative text passed to the logo avatar. The complete leading area is decorative and hidden from assistive technology.
avatarSrc
string
—
Primary avatar image URL, rendered in an Avatar at the xxlarge size.
avatarPrimaryContent
ReactNode
—
Content for the primary avatar slot. Overrides avatarSrc; the avatar owns the slot size.
avatarAlt
string
'Avatar'
Alternative text passed to the avatar. The complete leading area is decorative and hidden from assistive technology.
avatarSecondarySrc
string
—
Second image, which switches the avatar into its duo layout.
avatarSecondaryContent
ReactNode
—
Content for the secondary avatar slot. Also switches the avatar into its duo layout, and overrides avatarSecondarySrc.
avatarBadgeSrc
string
—
Image for the avatar’s image-badge slot.
avatarBadgeContent
ReactNode
—
Content for the avatar’s image-badge slot. Overrides avatarBadgeSrc.
statusIndicator
StatusIndicatorType
—
Status dot drawn on the avatar. Takes over the badge slot, so it replaces avatarBadgeSrc and avatarBadgeContent.
statusIndicatorIcon
IconSource
—
Glyph drawn over the status dot. Only renders when statusIndicator is also set.
primaryImageShape
AvatarShape
—
Shape of the primary avatar image.
placeholderStyle
PlaceholderStyle
—
Placeholder treatment used when an avatar image is missing.
listItems
ModalListItem[]
—
Icon-and-description rows shown instead of the subtitle. Ignored in title-only mode.
buttons
ReactNode
—
Actions slot below the content, normally a ButtonGroup.
isCarouselItem
boolean
false
Makes the modal focusable so focus can drive carousel selection.
className
string
''
Extra class on the modal panel.
style
CSSProperties
{}
Extra inline styles on the modal panel.

Notable inherited props

PropTypeDefaultDescription
shapeProvider
ShapeProvider
rounded rectangle at the medium corner radius
Geometry used for the panel’s clip path and every material path.
width
number \| string
—
Explicit width. Leave it unset; the modal already fills the width it is given.
height
number \| string
—
Explicit height. Leave it unset so the modal sizes to its content.
aria-label
string
the title, subtitle, and banner tag joined with commas
Accessible name for the panel.
onStateChange
(prev: InteractionState, next: InteractionState) => void
—
Fires when the interaction state changes. Only meaningful while the modal is focusable.
Standard div attributes are forwarded to the panel element, and the component forwards its ref there too. See Panel and Container for the rest of the inherited surface.

Types and enums

ModalContentMode

MemberDescription
STANDARD
Full presentation. Title in the heading appearance, with the leading area, subtitle or list, and actions all available.
TITLE_ONLY
Compact presentation. Title in the emphasized body appearance; the leading area, subtitle, and list are suppressed. Actions still render.

ModalBannerSize

MemberDescription
STANDARD
Banner at the 440:204 aspect ratio.
TALLER
Banner at the 440:300 aspect ratio.

ModalBannerTag

MemberDescription
NONE
No tag.
BETA
Draws the design system’s “Beta” Tag over the bottom of the banner.

ModalListItem

FieldTypeDescription
icon
IconSource
Required leading glyph for the row.
description
string
Required row text.
iconColor
IconTintColor
Optional semantic tint for the glyph. Free-form CSS colors are not accepted.

Behavior

  • One leading treatment. Banner, logo, avatar, and icon are mutually exclusive and resolve in that order: bannerSrc wins, then logoSrc, then avatarSrc or avatarPrimaryContent, then icon. Supplying several is not an error — the lower-priority ones simply do not render.
  • Content modes.TITLE_ONLY suppresses the whole leading area, the subtitle, and the list, and switches the title to the emphasized body appearance. The standard mode reserves a single-line minimum height for the title so stacked modals stay visually aligned.
  • Subtitle or list, not both. The subtitle renders only when it is a non-empty string, the mode is standard, and there are no list items. Supplying listItems always wins.
  • Banner tag. The tag renders only when a banner is present and bannerTag is not NONE, over a gradient scrim pinned to the bottom of the banner.
  • Long subtitle. The subtitle is clamped to four lines. When the text overflows, the modal shows a “Read more” affordance, becomes focusable, and becomes clickable; clicking it opens a full-screen overflow surface containing the complete subtitle, which closes on Escape. That surface is rendered into the app’s floating portal root, so it covers the route rather than the modal’s own box. Overflow is measured with a resize observer and re-measured when the subtitle changes.
  • Focus. The modal is not a focus stop by default. It becomes focusable when isCarouselItem is set or when the subtitle overflows. Press feedback is disabled, so a focusable modal never enters the pressed state. The controls inside buttons are the real focus stops, and focus navigation moves between them like any other controls on the route.
  • Not an overlay. No backdrop, no focus trap, no role="dialog", and no aria-modal. Escape is left unhandled so the surrounding route can act on it — the one exception is while the read-more surface is open, where Escape closes that surface and goes no further.
  • Sizing. The modal fills the width it is given and grows to fit its content. All internal spacing comes from spacing tokens, so the layout adapts to the space the route allocates.
  • Clipping. Content clipping is off, which lets inset media such as the banner draw its own rounded corners against the panel’s smooth corners.

Accessibility

  • The panel’s accessible name defaults to the visible title, subtitle, and banner tag joined with commas. Pass aria-label to replace it when the visible copy does not read well out of context.
  • The complete leading area — icon, banner, logo, and avatar — is marked decorative and hidden from assistive technology. The corresponding *Alt values do not contribute to the modal’s accessible name, so repeat any meaning carried by the media in the title, subtitle, or list.
  • The “Read more” affordance is decorative. The overflow surface it opens is a real dialog: it takes role="dialog", aria-modal="true", is labelled with the complete subtitle, receives focus when it opens, and closes on Escape.
  • The modal itself claims no dialog semantics. When separate presentation infrastructure overlays it, that presenter owns any dialog semantics, focus entry, focus containment, and returning focus to whatever opened it. A dedicated decision route owns its own focus entry and return navigation.
  • Every control in buttons supplies its own accessible name. Keep those labels explicit (“Delete photo”, not “OK”).
  • Handle system Back on the route with useBackNavigation (see Navigation and Focus) rather than adding an in-app back button to the modal.
  • Give the modal a title, and choose either subtitle or listItems.
  • Choose exactly one leading treatment so its precedence never becomes visible.
  • Put actions in buttons as a ButtonGroup so they share the group’s alignment and focus handling.
  • Put any meaning conveyed by the leading media into visible modal copy because the leading area is decorative in the accessibility tree.
  • Set isCarouselItem on every modal used as a Carousel item.
  • Tint list icons with a semantic IconTintColor case when the glyph carries status meaning.

Avoid

  • Avoid treating Modal as an overlay presenter. It paints no scrim, does not trap focus, and ignores Escape and system Back, so a dedicated route or separate presenter must own those behaviors.
  • Choose subtitle or listItems; when both are supplied, both do not appear.
  • Choose one of bannerSrc, logoSrc, avatarSrc, and icon for the leading treatment.
  • Preserve the modal’s material and clipping behavior, and avoid nested rounded surfaces that compete with its panel.
  • Keep listItems informational and place interactive controls in buttons.
  • Use Toast for brief confirmation of an action that already succeeded.