Container

Updated: Sep 24, 2026
Use Container for a custom actionable region that the toolkit does not already provide, such as a tappable status widget, a bespoke tile, or a media destination. For a standard row, prefer ListItem; for a standard action, prefer Button so the application stays consistent with the toolkit’s patterns.
Container is the toolkit’s primary interactive building block. It combines InteractableBase with a material. The component owns focus, press, activation, tooltips, and partial-focus feedback. It renders a ContainerMaterial behind and in front of its content and clips those layers to the shape provider’s geometry. Most interactive toolkit components build on this behavior.
The caller owns all content through children. Container does not provide title, subtitle, or icon convenience props, which keeps custom layouts flexible without changing the component’s interaction behavior.

Usage

import {Container, TextView, TextStyle, TextColor} from '@wearables-ui-toolkit/mrbd';

function ForecastTile() {
  return (
    <Container clickable onActivate={openForecast} ariaLabel="Today's forecast">
      <div className="forecast-tile">
        <TextView textStyle={TextStyle.BODY1}>Today's forecast</TextView>
        <TextView textStyle={TextStyle.META1} textColor={TextColor.SECONDARY}>
          CLEAR
        </TextView>
      </div>
    </Container>
  );
}
.forecast-tile {
  display: grid;
  gap: var(--uit-spacing-med-lg);
  padding: var(--uit-spacing-large);
}
Choose a different material and shape when the defaults do not fit the design:
import {useMemo} from 'react';
import {
  Container,
  CornerRadius,
  MaterialLibrary,
  RoundedRectangleShapeProvider,
} from '@wearables-ui-toolkit/mrbd';

// A shape provider is immutable, so a single instance can be shared.
const CARD_SHAPE = new RoundedRectangleShapeProvider(CornerRadius.MEDIUM);

function MediaCard() {
  // A material carries its own visual state, so each mounted container needs
  // an instance of its own.
  const material = useMemo(() => MaterialLibrary.card(), []);

  return (
    <Container clickable material={material} shapeProvider={CARD_SHAPE}>
      <CardBody />
    </Container>
  );
}
Keep both instances stable across renders so the toolkit can preserve cached geometry and material state. Each mounted container must receive its own material instance because materials carry mutable state; mounting one instance in two containers at the same time throws. Prefer useMemo for the material rather than a module constant. Shape providers are immutable, so callers can share them safely.
Container-based components such as Button, ListItem, ControlTile, and QuickReplyButton also expose a material prop. A material factory can therefore provide a consistent custom theme across several component types while each component retains its own layout, shape, interaction behavior, and accessibility. Create a fresh material instance for every mounted component, even when all of the instances come from the same factory. A material owns mutable visual state, so mounting one instance in multiple hosts violates its single-owner contract and throws. See Custom materials for layer composition patterns.
Use the polymorphic as prop to preserve native element semantics. Container forwards the ref to the rendered element:
import {Link} from 'react-router-dom';
import {Container} from '@wearables-ui-toolkit/mrbd';

<Container as={Link} to="/media/library" ariaLabel="Media library">
  <LibraryPreview />
</Container>;
Container is also available from its own subpath:
import {Container} from '@wearables-ui-toolkit/foundation/components/Container';

Props

Container extends InteractableBase. See InteractableBase for the complete interaction, tooltip, and focus API; notable inherited props appear below. The as prop makes Container polymorphic, and the component forwards remaining props to the rendered element. The full prop type is ContainerProps.

Own props

PropTypeDefaultDescription
children
ReactNode
—
Content, stacked above the background layers and below the foreground layers.
material
ContainerMaterial
the default interactive material
Draws the background and foreground layers for each visual state. One instance may be mounted by only one container at a time.
shapeProvider
ShapeProvider
rounded rectangle at the large corner radius
Geometry used for content clipping and for every material fill and stroke path.
width
number \| string
'auto'
CSS width. A number is treated as pixels and lets the shape path be computed before measurement.
height
number \| string
'auto'
CSS height, same rules as width.
clipContent
boolean
true
Clips the content subtree to the shape. Material layers are positioned independently and are not clipped by this.
interactionStateOverride
InteractionState
—
Sets the interaction state used for visuals and state-change animations. Use when a composite control needs the container to reflect another interaction target.
visualStateOverride
VisualState
—
Forces the rendered visual state instead of deriving it from interaction, and drives the material to that state.
materialTransition
ContainerMaterialTransition
—
Externally drives a material cross-fade as {from, to, progress}. progress is clamped to the 0–1 range, and a transition whose from and to match is ignored. When set, the internal transition is suppressed.
visualStateForInteractionStateFn
(state: InteractionState) => VisualState
default mapping
Overrides how interaction state maps to the rendered visual state.
contentScaleForStateFn
(state: State, size: {width: number; height: number}) => number
app platform profile
Overrides the content “breathing” scale for each state.
onScaleChange
(scale: number) => void
—
Called with the interaction scale whenever it changes, including once with the resting scale after mount. This is the interaction scale only; customScaleX and customScaleY are not folded in.
customScaleX
number
1
Multiplied into the horizontal scale.
customScaleY
number
1
Multiplied into the vertical scale.
customAlpha
number
1
Multiplied into the root opacity.
stateChangeAnimations
(context: ContainerStateChangeAnimationContext) => ContainerStateChangeAnimation[]
—
Extra animation participants driven on the same clock as the built-in state change, so custom motion stays in lockstep with the material.
ContainerMaterialTransition, ContainerStateChangeAnimation, and ContainerStateChangeAnimationContext are exported from the @wearables-ui-toolkit/foundation/components/Container subpath.
The stateChangeAnimations context includes prevInteraction, newInteraction, animated (false when the toolkit snaps the change), the selected transition duration and interpolator, and the measured containerSize. Each returned participant provides a duration, an optional startDelay, and onStart, onFrame, onComplete, and onCancel callbacks. onFrame receives elapsed milliseconds and linear progress.

Notable inherited props

PropTypeDefaultDescription
as
ElementType
'div'
Render as another element or component, such as a router link.
interactive
boolean
true
Default for focusable, pressable, and clickable.
focusable
boolean
value of interactive
Whether the container takes focus.
pressable
boolean
value of interactive
Whether the container shows press feedback.
clickable
boolean
value of interactive
Whether activation runs.
disabled
boolean
false
Blocks activation and fades the root; stays focusable.
initialFocusEligible
boolean
true
When false, the container and its subtree are skipped by automatic initial focus.
onActivate
(event: InteractableActivationEvent) => void
—
The action. Fires once per activation.
onStateChange
(prev: InteractionState, next: InteractionState) => void
—
Called after Container has applied its own state change.
ariaLabel
string
—
Accessible name.
tooltipMode
TooltipMode
TooltipMode.NONE
Enables the tooltip. The rest of the tooltip* props behave exactly as on the base.
partialFocusSupportedAxis
PartialFocusSupportedAxis
PartialFocusSupportedAxis.XY
Which axes get handoff and rubber-band feedback.
isRubberbandTranslationEnabled
boolean
true
Whether the rubber band moves the element or only the material origin.
onInvalidFocusDirection
(direction: InvalidFocusDirection) => void
—
Called in addition to the built-in rubber band.
onPartialFocusHandoff
(detail) => void
—
Called in addition to the built-in handoff feedback.

Behavior

Material and visual state

  • Container asks the material to render one of the four VisualState values. The default mapping translates interaction state, and visualStateForInteractionStateFn can customize that mapping.
  • When state changes, Container cross-fades from the previous material state to the next. It uses the same clock for material, scale, and opacity changes, which keeps the element’s motion coordinated.
  • interactionStateOverride makes the supplied value the source for focused, pressed, and disabled visuals. The container keeps its standard material, scale, opacity, and transition behavior.
  • Container applies material opacity only to material layers. Translucent materials therefore preserve content opacity; only disabled and customAlpha change root opacity.
  • The material owns its inset. When it declares an inset, Container positions the layers inside the element box and regenerates the shape path at the smaller size so strokes remain within the edge.
  • Container establishes a stacking context so background layers paint behind content and foreground layers paint in front. When a material declares backdrop blending, Container marks the root with data-uit-background-blends-with-backdrop and omits both the stacking context and a redundant identity transform. This lets the material blend with content behind it.
  • When the current material state is hidden or fully transparent, Container removes its layers from the tree instead of rendering invisible layers.

Content scale and press motion

  • Container scales content slightly inward at rest, expands it to full size on focus, and insets it again on press. This feedback rhythm has no hover state.
  • Container reads the platform profile supplied by App and uses it for the default content scale. Applications do not pass a platform value to individual containers. Use contentScaleForStateFn only when a component needs a different curve.
  • The toolkit computes scale from the measured border box so responsive and intrinsically sized containers respond correctly. Changes caused only by measurement—such as initial layout, font settling, or resize—apply immediately. Avoiding animation here prevents content from visibly growing into place on first paint.
  • Press-in and press-out transitions use separate durations and easing curves. A press-only scale change uses a spring that can overshoot and settle. A transition that also changes opacity uses the eased curve instead.
  • The caller can add scale or fade through customScaleX, customScaleY, and customAlpha, which multiply the toolkit’s interaction values. Providing transform through style gives the caller ownership of the transform and disables the built-in scale animation.
  • Changing disabled fades the root between enabled and disabled opacities on the same state-change animation.

Fast movement, page entry, and reduced motion

  • During fast focus movement, the toolkit snaps state changes instead of animating them. This prevents overlapping animation waves during quick list traversal.
  • During page entry, the toolkit also snaps state changes so arriving containers do not animate focus at the same time.
  • When the user prefers reduced motion, the toolkit applies state changes without animation.
  • In each snapped case, the toolkit still calls stateChangeAnimations with animated: false, but it does not schedule returned participants. Callers must read this flag and place custom visuals directly in their end state so reduced-motion and transition behavior remain correct.

Shape, smooth corners, and clipping

  • After size is available—either from numeric width and height values or measurement—the shape provider generates the smooth rounded path for content clipping and every material fill and stroke. Until then, the toolkit uses the provider’s plain CSS border radius as a fallback.
  • The toolkit derives geometry from the untransformed border box. Interaction scale or a caller-provided transform therefore leaves material paths at full size.
  • clipContent clips only the content subtree. Material effects such as glow strokes and drop shadows can remain visible outside the shape.
  • Keep inset children with rounded corners concentric with their owner. Choose the corner-radius token equal to the owner’s radius minus the inset token, or use full-bleed content. This preserves consistent corner geometry.

Partial focus and rubber band

  • During focus handoff, Container springs the partial-focus origin toward the other element and back. Materials use that value to shift their gradient origin, and the incoming element begins offset toward the previous focus. The element itself does not translate during handoff; only the material origin moves.
  • When directional navigation reaches a boundary, Container plays a two-stage rubber-band nudge toward the blocked direction and back. This is the only case where the element translates. isRubberbandTranslationEnabled={false} keeps the element still while the material origin continues to shift.
  • The rubber band runs only on axes allowed by partialFocusSupportedAxis. For example, a horizontal rail set to X ignores vertical boundaries.
  • Setting partialFocusSupportedAxis to None disables both effects and keeps the origin centered.

Sizing

  • Container is an inline flex box with intrinsic sizing by default. Prefer to let content and the surrounding responsive layout determine its size instead of hardcoding pixel dimensions.
  • Consider numeric width and height when the shape path must exist before the first measurement, such as for a fixed-size tile.

Accessibility

Container inherits the accessibility behavior of InteractableBase: a button role for clickable div roots, tab index management, aria-disabled, the ariaLabel alias, and a suppressed native focus outline. The material’s focused treatment provides the visible focus indicator.
Treat the entire container as one target. Its accessible name and visible content must describe the activation result whenever the target is focusable. This keeps the action understandable to screen-reader users and people using Meta Ray-Ban Display. When the target is not a button, provide the correct role and matching ARIA state props. Prefer as when a native element provides the appropriate semantics.
A non-interactive container (interactive={false}) adds no role, tab index, or aria-disabled. The application must provide appropriate semantics within its content so assistive technology can interpret it.
  • Inset authored, non-full-bleed content with var(--uit-spacing-large). Let full-bleed media use the container’s clipping.
  • Give each mounted container its own material and keep it stable with useMemo. Immutable shape providers can be shared.
  • Use interactionStateOverride only when a composite control needs its Container to reflect another interaction target’s focused, pressed, or disabled state.
  • Use visualStateOverride and materialTransition for coordinated behavior such as a driven preview or scrubbed transition.
  • Use as to preserve native navigation semantics.
  • Use stateChangeAnimations when custom motion must stay synchronized with the material cross-fade. Avoid a separate timer because it can drift from the toolkit’s interaction state.

Avoid

  • Do not nest Container, StaticContainer, Surface, Button, Chip, Tag, or Header inside a Container; these compositions are unsupported.
  • Do not place an independently focusable child inside a clickable container. Nested focus targets create ambiguous focus and activation behavior.
  • Avoid using visualStateOverride to imitate focus. Drive focus visuals from real interaction so the visible state matches accessibility focus.
  • Avoid storing a material in a module constant and sharing it across list rows. Two containers mounting the same material instance at once is a runtime error.
  • Avoid styling Container internals or targeting generated class names because the toolkit owns their structure and styling.
  • Prefer the existing toolkit component over rebuilding it with Container. If no component fits, consider requesting the missing library pattern.