TextSwitcher

Updated: Sep 24, 2026
Use TextSwitcher for a short, discrete value that changes in place, such as a capture mode, connection status, or step name. Its crossfade preserves visual continuity when a hard cut would feel abrupt.
The component owns the static material surface, text appearance, and transition. It also announces the new value unless the application provides an accessible name or overrides the live-region attributes. The caller supplies the string.

Usage

import {TextSwitcher} from '@wearables-ui-toolkit/mrbd';

function CaptureMode({mode}: {mode: string}) {
  return <TextSwitcher text={mode} />;
}
When the initial value is already correct, prefer noAnimationFirstView so it appears settled. Consider letting the surface fill its row so longer values do not resize the surrounding layout:
import {TextSwitcher} from '@wearables-ui-toolkit/mrbd';

function ConnectionStatus({status}: {status: string}) {
  return (
    <div className="status-row">
      <TextSwitcher
        className="status-switcher"
        text={status}
        noAnimationFirstView
      />
    </div>
  );
}
.status-row {
  display: flex;
  padding: var(--uit-spacing-large);
}

.status-switcher {
  flex: 1;
}
A custom shape can align the surface with the surrounding composition. When the application honors a reduced-motion preference, disable the crossfade:
import {
  CornerRadius,
  RoundedRectangleShapeProvider,
  TextSwitcher,
  usePrefersReducedMotion,
} from '@wearables-ui-toolkit/mrbd';

const SHAPE = new RoundedRectangleShapeProvider(CornerRadius.SMALL);

function StepName({step}: {step: string}) {
  const prefersReducedMotion = usePrefersReducedMotion();
  return (
    <TextSwitcher
      text={step}
      shapeProvider={SHAPE}
      animated={!prefersReducedMotion}
    />
  );
}

Props

The complete prop type is TextSwitcherProps. It extends StaticContainerProps without children or backgroundStyle. TextSwitcher owns its content and keeps its material visible, so it does not expose the background-style switch. See StaticContainer for the inherited surface props.
Standard React HTML attributes, including className, style, id, aria-*, data-*, and DOM event handlers, are forwarded to the root element. The ref is forwarded to that element as an HTMLDivElement.

Own props

PropTypeDefaultDescription
text
string
—
String displayed by the component. A change starts a crossfade. An omitted or empty string leaves the surface faded out with no content.
duration
number
300
Duration in milliseconds for each fade leg. A complete swap takes twice this value, which also sets the CSS transition duration.
noAnimationFirstView
boolean
false
Displays the first value immediately instead of fading it in. Later changes still crossfade.
animated
boolean
true
Controls whether text changes crossfade. The value applies to every change while set; no per-change override is available.

Notable inherited props

PropTypeDefaultDescription
material
ContainerMaterial
MaterialLibrary.defaultStatic()
Material painted behind the text.
shapeProvider
ShapeProvider
RoundedRectangleShapeProvider at CornerRadius.LARGE
Geometry used for clipping and for the material fill and stroke paths.
visualState
VisualState
VisualState.DEFAULT
Static visual state rendered by the material. It adds no interaction.
width
number \| string
'auto'
Explicit surface width. A number is treated as pixels.
height
number \| string
'auto'
Explicit surface height, using the same rules as width.
clipContent
boolean
true
Controls whether the shape clips the content.
useSmoothCorners
boolean
true
Controls whether corners use the shape provider’s smooth path instead of a plain radius.
contentClassName
string
''
Additional class for the content wrapper.
layoutTransitionToken
number
0
Change this value to request one coordinated resize of the surface, material, and clip.
layoutTransitionSignature
unknown
—
Change this value to request an animated size change instead of a snap. Values are compared by identity.
layoutTransitionAnchor
'start' \| 'end'
'start'
Edge that remains fixed while the surface resizes.
onLayoutTransitionComplete
() => void
—
Called when a requested layout transition finishes.

Behavior

Crossfade. When text changes, the component fades out the entire surface, swaps the string, and fades the surface back in. The root uses an ease-in-out opacity transition. An empty or undefined value fades the surface out and leaves it empty instead of displaying blank content.
Coalescing. The component queues changes received during a crossfade and applies only the newest queued value when the current crossfade finishes. A burst of updates therefore settles on the latest value without replaying each intermediate value.
Skipping the animation. With animated={false}, the component applies the string immediately, moves opacity directly to its final value, and sets the CSS transition to none. noAnimationFirstView applies this behavior only to the first value; later changes retain the opacity transition and crossfade.
Reduced motion.TextSwitcher does not read the motion preference. The application owns that preference and should pass animated={false} when the user prefers reduced motion. usePrefersReducedMotion provides the signal.
Text rendering. The component renders one centered line with an end ellipsis, the body2 appearance, the primary text color, and fixed internal padding. The appearance, color, alignment, and padding are not configurable. The padding gives the surface a minimum size, including for very short values.
Sizing. The inline flex root sizes to its text, so the surface grows or shrinks with the value unless the caller sets width. With clipContent enabled, the shape clips the content. When value lengths vary, prefer a fixed width or update layoutTransitionToken or layoutTransitionSignature so the resize animates consistently.
Style merging. The caller’s style object is merged after the component’s opacity and transition. Setting either property in style replaces the crossfade behavior, so avoid overriding them.
Memoization. The component is memoized. Prefer stable style and material objects so it can skip unnecessary renders; an inline object literal creates a new reference on each render.

Accessibility

  • Without an aria-label, the root is a polite, atomic live region (aria-live="polite", aria-atomic="true"). This allows the settled value to be announced once.
  • A non-empty aria-label suppresses the automatic live region. The label then owns the accessible name, and the application must provide any announcement of value changes so assistive technology receives the update.
  • Caller-provided aria-live and aria-atomic values take precedence over the automatic values. Use aria-live="assertive" only when the update is urgent, or aria-live="off" when another element owns the announcement.
  • The surface has no role and is not focusable. Connect it to the control or region whose state it reports, typically by setting an id on the switcher and referencing it with aria-describedby.
  • A swap briefly makes the value invisible. Do not use TextSwitcher for content a user may be reading, because the transition would interrupt access to that content.
  • Prefer TextSwitcher for a short value that benefits from visual continuity.
  • Set noAnimationFirstView when a known initial value should appear settled.
  • Give the surface an explicit width or a layout transition signature when a changing value would otherwise shift nearby content.
  • Disable animation when the user prefers reduced motion.

Avoid

  • Avoid paragraphs, wrapped copy, or rapidly changing telemetry. The component displays one clipped line, and each change requires a complete crossfade.
  • Avoid setting opacity or transition through style; the component owns those properties for its crossfade.
  • Avoid wrapping the component in competing position or opacity animations, which can make the transition inconsistent.
  • TextSwitcher does not accept children. Do not nest a container-like component, button, chip, tag, or page header inside it.
  • Choose one owner for announcements. Avoid relying on the automatic announcement after supplying a label that disables it.