Interactions

Updated: Sep 24, 2026
Use the interaction module when authoring a component with InteractableBase, overriding Container’s state mapping, or creating a custom material. In application code, prefer component props that express user intent rather than forcing a visual state.
The module defines the state vocabulary shared by interactive toolkit components and materials. It separates a control’s logical state (State plus a disabled flag) from the state rendered by a material (VisualState). This separation lets the toolkit evolve behavior and appearance independently while keeping interaction feedback consistent.
Import the platform interaction APIs from the Meta Ray-Ban Display package root and the shared content-scale helpers from the Foundation package root:
import {
  ContentScaleInsets,
  State,
  VisualState,
  getContentScaleForState,
} from '@wearables-ui-toolkit/mrbd';
import {
  DefaultContentScaleInsets,
  getDefaultContentScaleForState,
} from '@wearables-ui-toolkit/foundation';
import type {
  InteractionState,
  Platform,
} from '@wearables-ui-toolkit/mrbd';
import type {ContentScaleForStateFn} from '@wearables-ui-toolkit/foundation';
State and VisualState are each exported as a value and a type. For example, State.FOCUSED and state: State work without separate imports.
Override the mapping from interaction state to material appearance:
import {Container, State, VisualState} from '@wearables-ui-toolkit/mrbd';
import type {InteractionState} from '@wearables-ui-toolkit/mrbd';

function StatusTile() {
  return (
    <Container
      clickable
      onActivate={openStatus}
      ariaLabel="Status"
      visualStateForInteractionStateFn={(interaction: InteractionState) => {
        // Render nothing at rest; light up only on focus and press.
        if (interaction.isDisabled) {
          return VisualState.DEFAULT;
        }
        switch (interaction.state) {
          case State.FOCUSED:
            return VisualState.FOCUSED;
          case State.PRESSED:
            return VisualState.PRESSED;
          default:
            return VisualState.NONE;
        }
      }}
    >
      <StatusContent />
    </Container>
  );
}
Follow a control’s state from a sibling:
import {Container, State} from '@wearables-ui-toolkit/mrbd';
import type {InteractionState} from '@wearables-ui-toolkit/mrbd';
import {useState} from 'react';

function LabelledTarget() {
  const [isActive, setIsActive] = useState(false);

  return (
    <div className="labelled-target">
      <Container
        clickable
        ariaLabel="Capture"
        onActivate={capture}
        onStateChange={(_previous: InteractionState, next: InteractionState) => {
          setIsActive(next.state !== State.DEFAULT);
        }}
      >
        <CaptureGlyph />
      </Container>
      <CaptureCaption isEmphasized={isActive} />
    </div>
  );
}

Types and enums

State

State describes the control’s logical interaction state. Exactly one member is active at a time.
MemberValueMeaning
State.DEFAULT
'default'
Resting. Neither focused nor pressed.
State.FOCUSED
'focused'
The control owns focus.
State.PRESSED
'pressed'
The control is being pressed, by pointer or by Enter/Space.
Press takes precedence over focus. A focused control reports PRESSED while it is being pressed, then returns to FOCUSED when the press ends if it still owns focus.

VisualState

VisualState describes the state rendered by a material. It adds NONE for a material that draws nothing.
MemberValueMeaning
VisualState.NONE
'none'
No material is drawn.
VisualState.DEFAULT
'default'
Resting appearance.
VisualState.FOCUSED
'focused'
Focused appearance.
VisualState.PRESSED
'pressed'
Pressed appearance.

InteractionState

interface InteractionState {
  state: State;
  isDisabled: boolean;
}
A disabled control continues to move through default, focused, and pressed states, so disabled is a separate flag rather than a fourth State member. Every interactive component passes this shape to onStateChange as (previous, next).

Platform

type Platform = 'mrbd';
Platform identifies the target for a content-scale lookup. The package targets Meta Ray-Ban Display, so this closed union has one member. App supplies 'mrbd' through platform context, and components such as Container and Surface read it automatically. Use Platform when calling a platform-aware helper directly, and read values from ContentScaleInsets rather than maintaining a separate table.

ContentScaleInsets

ContentScaleInsets provides the horizontal content-scale inset for each Platform and State:
ContentScaleInsets[platform][state].horizontal;
getContentScaleForState(state, platform, size) applies the same profile and returns the uniform scale factor for the supplied element size.
DefaultContentScaleInsets is the foundation-level form of the same default profile, without a platform key. getDefaultContentScaleForState(state, size) calculates its uniform scale directly and satisfies ContentScaleForStateFn, whose signature is:
type ContentScaleForStateFn = (
  state: State,
  containerSize: {width: number; height: number},
) => number;
Use ContentScaleInsets and getContentScaleForState when implementing custom content-scale behavior. Container and Surface already use the profile supplied by App; override contentScaleForStateFn only when a component needs a different curve.
Interactive containers scale content slightly inward at rest, expand it to full size on focus, and inset it again on press. This breathing feedback replaces a hover style for Meta Ray-Ban Display. DEFAULT and PRESSED share an inset, while FOCUSED has no inset and therefore renders at full size.
The table contains horizontal insets only. The toolkit derives one scale factor from the element width and applies it uniformly to both axes, so wide controls breathe proportionally less than narrow controls. Use this table when a component that does not use Container needs the same motion rhythm. Avoid hardcoded duplicate values because they can make controls respond inconsistently. When a Container-based component needs a different curve, override contentScaleForStateFn instead of adding a second scale.

Behavior

  • Disabled is orthogonal to state. A disabled control reports DEFAULT, FOCUSED, or PRESSED with isDisabled: true. It remains focusable so directional navigation does not leave a hole in the layout, and it renders its resting visual state at the disabled opacity.
  • Default mapping. Unless a component overrides the mapping, DEFAULT maps to VisualState.DEFAULT, FOCUSED maps to VisualState.FOCUSED, PRESSED maps to VisualState.PRESSED, and every disabled state maps to VisualState.DEFAULT.
  • NONE is a material decision, not a logical state. No control reports NONE as its logical state. A component or material selects it to suppress drawing, as StaticContainer does for its NONE background style.
  • Press feedback has a minimum duration. The toolkit briefly holds the pressed visual state after an immediate release so a fast tap remains visible. Programmatic activation without a pointer press produces the same brief pulse.
  • Fast focus movement snaps the transition. The toolkit classifies focus changes in quick succession, such as a sweep down a long list, as fast movement. Material-backed components apply those state changes without animation so the sweep does not create overlapping transitions. This classification is shared across the application and decays shortly after the sweep stops. Entering the pressed state resets fast-movement decay, but the most recent focus interval can remain classified as fast, so a press during the sweep can still snap.
  • Zero and negative sizes are safe. A content-scale lookup for an element without a measured width returns full size instead of a fabricated scale. This prevents a component from animating out of an invalid resting scale on first paint.
  • A visible tooltip can mask focus state. When an interactable uses tooltipHidesFocusState, onStateChange reports DEFAULT while the tooltip is open. This keeps the anchor from rendering focus under the tooltip. The control still owns focus; only its reported state is masked.
  • Read InteractionState in onStateChange when a sibling needs to follow a control’s state.
  • Use VisualState when authoring a material or custom state mapping.
  • Prefer exported members over their raw string values for type safety and consistency.
  • Handle isDisabled before switching on state in a custom mapping.

Avoid

  • Avoid a parallel state machine for focus and press. The interactable owns that behavior so input and visual feedback remain aligned.
  • Do not force a visual state to imitate focus. Drive focus appearance from real interaction so assistive technology, directional navigation, and visuals agree.
  • Treat disabled as the separate isDisabled flag, not as a fourth State member.
  • Use ContentScaleInsets for resting and pressed insets instead of defining new values, so controls share the same breathing rhythm.