Skip to content

Navigation

Top app bars

The bar at the top of a screen with its title and actions: small, center-aligned, medium and large flexible, and two-rows, with scroll behaviors.

Examples

Variants

Small, center-aligned, medium flexible, two rows and large flexible. The two-rows bars take a subtitle, and the title is a heading at the level you pick (headingLevel, default 1).

Small

Center-aligned

Medium flexible

With a subtitle

Two rows

Large flexible

React HTML
import { Icon, IconButton, TopAppBar } from '@kyralabs/m3exp-web/react';

const menu = (
  <IconButton aria-label="Menu">
    <Icon name="menu" />
  </IconButton>
);
const actions = (
  <>
    <IconButton aria-label="Search">
      <Icon name="search" />
    </IconButton>
    <IconButton aria-label="More options">
      <Icon name="more_vert" />
    </IconButton>
  </>
);

export default function Variants() {
  return (
    <div style={{ display: 'grid', gap: 'var(--m3e-sys-space-200)', inlineSize: '100%' }}>
      <TopAppBar headingLevel={4} title="Small" navigationIcon={menu} actions={actions} />
      <TopAppBar
        headingLevel={4}
        variant="center-aligned"
        title="Center-aligned"
        navigationIcon={menu}
        actions={actions}
      />
      <TopAppBar
        headingLevel={4}
        variant="medium-flexible"
        title="Medium flexible"
        subtitle="With a subtitle"
        navigationIcon={menu}
        actions={actions}
      />
      <TopAppBar
        headingLevel={4}
        variant="two-rows"
        title="Two rows"
        navigationIcon={menu}
        actions={actions}
      />
      <TopAppBar
        headingLevel={4}
        variant="large-flexible"
        title="Large flexible"
        navigationIcon={menu}
        actions={actions}
      />
    </div>
  );
}
<div style="display: grid; gap: var(--m3e-sys-space-200); inline-size: 100%">
  <header class="m3e-top-app-bar">
    <div class="m3e-top-app-bar__navigation">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Menu">
        <span class="m3e-icon" aria-hidden="true">menu</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__headline">
      <h4 class="m3e-top-app-bar__title">Small</h4>
    </div>
    <div class="m3e-top-app-bar__actions">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Search">
        <span class="m3e-icon" aria-hidden="true">search</span>
      </button>
      <button
        type="button"
        class="m3e-icon-button m3e-icon-button--standard"
        aria-label="More options"
      >
        <span class="m3e-icon" aria-hidden="true">more_vert</span>
      </button>
    </div>
  </header>
  <header class="m3e-top-app-bar m3e-top-app-bar--center-aligned">
    <div class="m3e-top-app-bar__navigation">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Menu">
        <span class="m3e-icon" aria-hidden="true">menu</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__headline">
      <h4 class="m3e-top-app-bar__title">Center-aligned</h4>
    </div>
    <div class="m3e-top-app-bar__actions">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Search">
        <span class="m3e-icon" aria-hidden="true">search</span>
      </button>
      <button
        type="button"
        class="m3e-icon-button m3e-icon-button--standard"
        aria-label="More options"
      >
        <span class="m3e-icon" aria-hidden="true">more_vert</span>
      </button>
    </div>
  </header>
  <header class="m3e-top-app-bar m3e-top-app-bar--medium-flexible">
    <div class="m3e-top-app-bar__navigation">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Menu">
        <span class="m3e-icon" aria-hidden="true">menu</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__headline" aria-hidden="true">
      <h4 class="m3e-top-app-bar__title">Medium flexible</h4>
      <p class="m3e-top-app-bar__subtitle">With a subtitle</p>
    </div>
    <div class="m3e-top-app-bar__actions">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Search">
        <span class="m3e-icon" aria-hidden="true">search</span>
      </button>
      <button
        type="button"
        class="m3e-icon-button m3e-icon-button--standard"
        aria-label="More options"
      >
        <span class="m3e-icon" aria-hidden="true">more_vert</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__expanded">
      <div class="m3e-top-app-bar__headline">
        <h4 class="m3e-top-app-bar__title">Medium flexible</h4>
        <p class="m3e-top-app-bar__subtitle">With a subtitle</p>
      </div>
    </div>
  </header>
  <header class="m3e-top-app-bar m3e-top-app-bar--two-rows">
    <div class="m3e-top-app-bar__navigation">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Menu">
        <span class="m3e-icon" aria-hidden="true">menu</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__headline" aria-hidden="true">
      <h4 class="m3e-top-app-bar__title">Two rows</h4>
    </div>
    <div class="m3e-top-app-bar__actions">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Search">
        <span class="m3e-icon" aria-hidden="true">search</span>
      </button>
      <button
        type="button"
        class="m3e-icon-button m3e-icon-button--standard"
        aria-label="More options"
      >
        <span class="m3e-icon" aria-hidden="true">more_vert</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__expanded">
      <div class="m3e-top-app-bar__headline">
        <h4 class="m3e-top-app-bar__title">Two rows</h4>
      </div>
    </div>
  </header>
  <header class="m3e-top-app-bar m3e-top-app-bar--large-flexible">
    <div class="m3e-top-app-bar__navigation">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Menu">
        <span class="m3e-icon" aria-hidden="true">menu</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__headline" aria-hidden="true">
      <h4 class="m3e-top-app-bar__title">Large flexible</h4>
    </div>
    <div class="m3e-top-app-bar__actions">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Search">
        <span class="m3e-icon" aria-hidden="true">search</span>
      </button>
      <button
        type="button"
        class="m3e-icon-button m3e-icon-button--standard"
        aria-label="More options"
      >
        <span class="m3e-icon" aria-hidden="true">more_vert</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__expanded">
      <div class="m3e-top-app-bar__headline">
        <h4 class="m3e-top-app-bar__title">Large flexible</h4>
      </div>
    </div>
  </header>
</div>

Run the HTML version (CSS classes only).

Collapsing on scroll

Exit-until-collapsed: the expanded row collapses to the small bar as the content scrolls, and settles on a spring when scrolling stops. In HTML, attachTopAppBarScroll runs the same code. The other behaviors are pinned and enter-always.

Inbox

20 messages

  • Message 1
  • Message 2
  • Message 3
  • Message 4
  • Message 5
  • Message 6
  • Message 7
  • Message 8
  • Message 9
  • Message 10
  • Message 11
  • Message 12
  • Message 13
  • Message 14
  • Message 15
  • Message 16
  • Message 17
  • Message 18
  • Message 19
  • Message 20
React HTML
import { useRef } from 'react';
import { Icon, IconButton, TopAppBar } from '@kyralabs/m3exp-web/react';

const ITEMS = Array.from({ length: 20 }, (_, i) => `Message ${String(i + 1)}`);

export default function Scroll() {
  const scroller = useRef<HTMLDivElement>(null);
  return (
    <div
      ref={scroller}
      tabIndex={0}
      role="region"
      aria-label="Inbox, scrollable"
      style={{
        overflow: 'auto',
        inlineSize: '100%',
        maxInlineSize: 'var(--m3e-sys-window-width-medium)',
        blockSize: 'var(--m3e-sys-window-height-medium)',
      }}
    >
      <TopAppBar
        headingLevel={4}
        variant="large-flexible"
        title="Inbox"
        subtitle="20 messages"
        scrollBehavior="exit-until-collapsed"
        scrollTarget={scroller}
        navigationIcon={
          <IconButton aria-label="Back">
            <Icon name="arrow_back" />
          </IconButton>
        }
        actions={
          <IconButton aria-label="Search">
            <Icon name="search" />
          </IconButton>
        }
        style={{ position: 'sticky', insetBlockStart: 0, zIndex: 1 }}
      />
      <ul style={{ margin: 0, padding: 'var(--m3e-sys-space-200)' }}>
        {ITEMS.map((item) => (
          <li key={item} style={{ paddingBlock: 'var(--m3e-sys-space-150)' }}>
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
}
<div
  id="inbox"
  tabindex="0"
  role="region"
  aria-label="Inbox, scrollable"
  style="
    overflow: auto;
    inline-size: 100%;
    max-inline-size: var(--m3e-sys-window-width-medium);
    block-size: var(--m3e-sys-window-height-medium);
  "
>
  <header
    class="m3e-top-app-bar m3e-top-app-bar--large-flexible"
    style="position: sticky; inset-block-start: 0; z-index: 1"
  >
    <div class="m3e-top-app-bar__navigation">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Back">
        <span class="m3e-icon" aria-hidden="true">arrow_back</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__headline" aria-hidden="true">
      <h4 class="m3e-top-app-bar__title">Inbox</h4>
      <p class="m3e-top-app-bar__subtitle">20 messages</p>
    </div>
    <div class="m3e-top-app-bar__actions">
      <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Search">
        <span class="m3e-icon" aria-hidden="true">search</span>
      </button>
    </div>
    <div class="m3e-top-app-bar__expanded">
      <div class="m3e-top-app-bar__headline">
        <h4 class="m3e-top-app-bar__title">Inbox</h4>
        <p class="m3e-top-app-bar__subtitle">20 messages</p>
      </div>
    </div>
  </header>
  <ul style="margin: 0; padding: var(--m3e-sys-space-200)">
    <li style="padding-block: var(--m3e-sys-space-150)">Message 1</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 2</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 3</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 4</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 5</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 6</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 7</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 8</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 9</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 10</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 11</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 12</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 13</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 14</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 15</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 16</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 17</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 18</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 19</li>
    <li style="padding-block: var(--m3e-sys-space-150)">Message 20</li>
  </ul>
</div>
<script type="module">
  import { attachTopAppBarScroll } from '@kyralabs/m3e-elements/top-app-bar.js';

  const scroller = document.getElementById('inbox');
  attachTopAppBarScroll(scroller.querySelector('.m3e-top-app-bar'), {
    behavior: 'exit-until-collapsed',
    target: scroller,
    signal: new AbortController().signal,
  });
</script>

Run the HTML version (CSS classes only).

API

React

<TopAppBar>

Top app bar (COMPONENT_SPEC §11): the .m3e-top-app-bar contract in a <header>. Two-rows variants render the collapsed title in the top row (hidden from accessibility until the bar is half collapsed) and the expanded one in __expanded. Scroll behaviors run m3e-elements' attachTopAppBarScroll, so both layers write the same attributes.

Prop Type Default Description
variant "small" | "center-aligned" | "medium-flexible" | "large-flexible" | "two-rows" small Default small.
title required TopAppBarContent A function renders per row (Compose TwoRowsTopAppBar title(expanded)); single-row bars call it with false.
navigationIcon ReactNode Usually one <IconButton>.
actions ReactNode Usually <IconButton>s.
titleHorizontalAlignment 'start' | 'center' Horizontal alignment of the title and subtitle (Compose titleHorizontalAlignment). Default start; center-aligned bars are always centered.
collapsedHeight number | string Two-rows bars: the collapsed height, default 64 (TopAppBarDefaults.*CollapsedHeight). A number is in px.
expandedHeight number | string Two-rows bars: the expanded height, default per variant and subtitle (TopAppBarDefaults.*ExpandedHeight). A number is in px.
scrollBehavior "enter-always" | "pinned" | "exit-until-collapsed" pinned: the container color changes when content scrolls under a single-row bar. enter-always: the bar (a two-rows bar: its expanded row) also hides on scroll down and returns on any scroll up. exit-until-collapsed: it collapses on scroll down and expands when the content returns to its start (the two-rows bars' behavior in Compose's samples). Default: none (static bar).
scrollTarget RefObject<HTMLElement | null> The scrolling element the bar sits over. Default: the document.
headingLevel 1 | 2 | 3 | 4 | 5 | 6 Heading level of the title (COMPONENT_SPEC §11: chosen by the consumer). Default 1.
subtitle TopAppBarContent

Also accepts HTML attributes not listed here.

CSS

top-app-bar.css

@import '@kyralabs/m3e-css/top-app-bar.css';

Top app bars (COMPONENT_SPEC §11): small (Compose TopAppBar), center-aligned (CenterAlignedTopAppBar, or any bar with a centered title: titleHorizontalAlignment), medium and large flexible (MediumFlexibleTopAppBar, LargeFlexibleTopAppBar) and two-rows (TwoRowsTopAppBar). No JavaScript needed for the static bars; scroll behaviors (m3e-elements attachTopAppBarScroll, m3e-react <TopAppBar>) only set data-scroll-behavior, data-scrolled, --_m3e-top-app-bar-offset and, on two-rows bars, the collapsed-fraction values below.

<header class="m3e-top-app-bar">
  <div class="m3e-top-app-bar__navigation"><button class="m3e-icon-button …">…</button></div>
  <div class="m3e-top-app-bar__headline">
    <h1 class="m3e-top-app-bar__title">Title</h1>
    <p class="m3e-top-app-bar__subtitle">Subtitle</p>
  </div>
  <div class="m3e-top-app-bar__actions">…icon buttons…</div>
</header>

Two-rows bars (--medium-flexible, --large-flexible, --two-rows) add the expanded row; the top row's headline is the collapsed title (hidden from accessibility while the bar is less than half collapsed, as Compose does; the scroll behaviors swap aria-hidden at half):

<header class="m3e-top-app-bar m3e-top-app-bar--medium-flexible">
  <div class="m3e-top-app-bar__navigation">…</div>
  <div class="m3e-top-app-bar__headline" aria-hidden="true">…same title (small styles)…</div>
  <div class="m3e-top-app-bar__actions">…</div>
  <div class="m3e-top-app-bar__expanded">
    <div class="m3e-top-app-bar__headline">
      <h1 class="m3e-top-app-bar__title">Title</h1>
      <p class="m3e-top-app-bar__subtitle">Subtitle</p>
    </div>
  </div>
</header>

Custom two-rows heights (Compose collapsedHeight / expandedHeight): set --m3e-comp-top-app-bar-container-collapsed-height / --m3e-comp-top-app-bar-container-expanded-height on the bar (or an ancestor). The private --_m3e-top-app-bar-collapsed-height / --_m3e-top-app-bar-expanded-height (what <TopAppBar collapsedHeight expandedHeight> writes inline) still work and win over them; setting them yourself is deprecated since 1.8 (removed from the documented API in 2.0).

54 custom properties it reads
Custom property Default
--m3e-comp-app-bar-container-color var(--m3e-sys-color-surface)
--m3e-comp-app-bar-container-shape var(--m3e-sys-shape-corner-none)
--m3e-comp-app-bar-icon-button-space 0
--m3e-comp-app-bar-large-flexible-subtitle-font var(--m3e-sys-typescale-title-medium-font)
--m3e-comp-app-bar-large-flexible-subtitle-line-height var(--m3e-sys-typescale-title-medium-line-height)
--m3e-comp-app-bar-large-flexible-subtitle-size var(--m3e-sys-typescale-title-medium-size)
--m3e-comp-app-bar-large-flexible-subtitle-tracking var(--m3e-sys-typescale-title-medium-tracking)
--m3e-comp-app-bar-large-flexible-subtitle-weight var(--m3e-sys-typescale-title-medium-weight)
--m3e-comp-app-bar-large-flexible-title-font var(--m3e-sys-typescale-display-small-font)
--m3e-comp-app-bar-large-flexible-title-line-height var(--m3e-sys-typescale-display-small-line-height)
--m3e-comp-app-bar-large-flexible-title-size var(--m3e-sys-typescale-display-small-size)
--m3e-comp-app-bar-large-flexible-title-tracking var(--m3e-sys-typescale-display-small-tracking)
--m3e-comp-app-bar-large-flexible-title-weight var(--m3e-sys-typescale-display-small-weight)
--m3e-comp-app-bar-leading-icon-color var(--m3e-sys-color-on-surface)
--m3e-comp-app-bar-medium-flexible-subtitle-font var(--m3e-sys-typescale-label-large-font)
--m3e-comp-app-bar-medium-flexible-subtitle-line-height var(--m3e-sys-typescale-label-large-line-height)
--m3e-comp-app-bar-medium-flexible-subtitle-size var(--m3e-sys-typescale-label-large-size)
--m3e-comp-app-bar-medium-flexible-subtitle-tracking var(--m3e-sys-typescale-label-large-tracking)
--m3e-comp-app-bar-medium-flexible-subtitle-weight var(--m3e-sys-typescale-label-large-weight)
--m3e-comp-app-bar-medium-flexible-title-font var(--m3e-sys-typescale-headline-medium-font)
--m3e-comp-app-bar-medium-flexible-title-line-height var(--m3e-sys-typescale-headline-medium-line-height)
--m3e-comp-app-bar-medium-flexible-title-size var(--m3e-sys-typescale-headline-medium-size)
--m3e-comp-app-bar-medium-flexible-title-tracking var(--m3e-sys-typescale-headline-medium-tracking)
--m3e-comp-app-bar-medium-flexible-title-weight var(--m3e-sys-typescale-headline-medium-weight)
--m3e-comp-app-bar-on-scroll-container-color var(--m3e-sys-color-surface-container)
--m3e-comp-app-bar-small-container-height 64px
--m3e-comp-app-bar-small-subtitle-font var(--m3e-sys-typescale-label-medium-font)
--m3e-comp-app-bar-small-subtitle-line-height var(--m3e-sys-typescale-label-medium-line-height)
--m3e-comp-app-bar-small-subtitle-size var(--m3e-sys-typescale-label-medium-size)
--m3e-comp-app-bar-small-subtitle-tracking var(--m3e-sys-typescale-label-medium-tracking)
--m3e-comp-app-bar-small-subtitle-weight var(--m3e-sys-typescale-label-medium-weight)
--m3e-comp-app-bar-small-title-font var(--m3e-sys-typescale-title-large-font)
--m3e-comp-app-bar-small-title-line-height var(--m3e-sys-typescale-title-large-line-height)
--m3e-comp-app-bar-small-title-size var(--m3e-sys-typescale-title-large-size)
--m3e-comp-app-bar-small-title-tracking var(--m3e-sys-typescale-title-large-tracking)
--m3e-comp-app-bar-small-title-weight var(--m3e-sys-typescale-title-large-weight)
--m3e-comp-app-bar-subtitle-color var(--m3e-sys-color-on-surface-variant)
--m3e-comp-app-bar-title-color var(--m3e-sys-color-on-surface)
--m3e-comp-app-bar-trailing-icon-color var(--m3e-sys-color-on-surface-variant)
--m3e-comp-divider-thickness 1px
--m3e-comp-minimum-interactive-component-size 48px
--m3e-comp-top-app-bar-expanded-height 64px
--m3e-comp-top-app-bar-horizontal-padding 4px
--m3e-comp-top-app-bar-large-collapsed-height 64px
--m3e-comp-top-app-bar-large-flexible-expanded-height 120px
--m3e-comp-top-app-bar-large-flexible-with-subtitle-expanded-height 152px
--m3e-comp-top-app-bar-large-title-bottom-padding 28px
--m3e-comp-top-app-bar-medium-collapsed-height 64px
--m3e-comp-top-app-bar-medium-flexible-expanded-height 112px
--m3e-comp-top-app-bar-medium-flexible-with-subtitle-expanded-height 136px
--m3e-comp-top-app-bar-title-inset 12px
--m3e-comp-top-app-bar-two-rows-collapsed-height 64px
--m3e-comp-top-app-bar-two-rows-expanded-height 112px
--m3e-comp-top-app-bar-two-rows-with-subtitle-expanded-height 136px

Accessibility

<header>; heading level by consumer. Two-rows bars render the title in both rows; the row Compose hides from accessibility carries aria-hidden="true" (static markup: the collapsed headline), swapped by the scroll behavior at half collapsed.

Compose shrinks the bar's layout height (the expanded title moving up with the shrinking row, clipped); here the bar keeps its layout box and is clipped (clip-path), the expanded title translated up by the offset (a single-row bar's content by half of it, centered in what is left). Focus moving into a hidden part (a hidden single-row bar, or a focusable inside a collapsed expanded row) snaps the bar open; a two-rows bar's top row never hides.

Every interactive part has a target of at least 48 × 48 px, a visible focus ring for keyboard focus only, and works under forced-colors and prefers-reduced-motion.

Differences from Compose

the bar keeps its layout box and is clipped instead of shrinking, so content under a non-overlaid bar does not gain the space, and after a snap to collapsed near the top the clipped space stays empty until the content scrolls; no fling continuation (flingAnimationSpec: the browser's own momentum scrolls the content) and no dragging the bar itself (appBarDragModifier); exit-until-collapsed hands the bar the part of an upward scroll within the collapsed distance of the top instead of the scroll the content could not consume (the web cannot hold back the content's scroll); the expanded title's baseline padding comes from the web font's metrics (Compose uses the platform font's); a title row taller than the default heights (large fonts, wrapping) grows its row, but a two-rows title whose descenders exceed the row by the font's descent is clipped; titleHorizontalAlignment takes start and center (Compose accepts any Alignment.Horizontal); no TopAppBarColors customization API beyond overriding the tokens; no touch-exploration (TalkBack) detection.

Sources

Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from m3.material.io, as cited below from COMPONENT_SPEC §11 and the m3e-css sources.

AppBar.kt (vendored), AppBarTokens.kt, AppBarSmallTokens.kt, AppBarMediumFlexibleTokens.kt, AppBarLargeFlexibleTokens.kt, AppBarSamples.kt (vendored), animation-core Easing.kt (vendored).

AppBarTokens.kt, AppBarSmallTokens.kt, AppBarMediumFlexibleTokens.kt, AppBarLargeFlexibleTokens.kt@19991f2c plus the TopAppBarDefaults values and private AppBar.kt literals Compose renders (components/top-app-bar-defaults.css; SOURCES register #29). There is no AppBarCenterAligned* token object: the center-aligned bar uses AppBarSmallTokens. Supersedes the earlier TODO(verify) heights, padding and title type.

When the expanded headline does not fit above the padding, Compose reduces the padding (adjustedBottomPadding, L3227–3242), so with the default heights and title styles every flexible bar renders its expanded title at the top of the expanded row; the padding applies with taller custom heights. Rows grow with a taller headline (max(height, title height)). Snap spring TopAppBarDefaults.snapAnimationSpec = default effects (L1539–1540, for every variant: the flexible bars use the same scroll behaviors), jumping under reduced motion (project policy); the two-rows easings TopTitleAlphaEasing = cubic-bezier(0.8, 0, 0.8, 0.15) (L3604) and FastOutLinearInEasing = cubic-bezier(0.4, 0, 1, 1) (animation-core Easing.kt L66).

top-app-bar.css sources

(compose/material3 @ 19991f2c, AppBar.kt):

  • Height AppBarSmallTokens.ContainerHeight (TopAppBarExpandedHeight); the bar grows if the headline is taller (TopAppBarMeasurePolicy: max(height, title height)).
  • TopAppBarLayout: navigation icon padded start by TopAppBarHorizontalPadding, title padded by it on both sides and starting at max(TopAppBarTitleInset, navigation width), actions padded end (top-app-bar-defaults.css). Everything is vertically centered.
  • Colors: defaultTopAppBarColors (AppBarTokens). The container switches to OnScrollContainerColor once content scrolls under the bar, animated with the default effects spring (SingleRowTopAppBar). No elevation change.
  • Enter-always: the bar's height shrinks by the height offset, content stays centered in what is left and is clipped (layoutHeight = height + heightOffset, clipToBounds). Here the box keeps its height (so nothing reflows) and is clipped instead, with the content shifted by half the offset. Exit-until-collapsed moves a single-row bar the same way.
  • Center-aligned: SingleRowTopAppBar with Alignment.CenterHorizontally (placeTopAppBar centers the title on the whole width, then moves it aside for the navigation / actions).
  • Two-rows (TwoRowsTopAppBar): a top row of collapsedHeight (navigation, collapsed title in the small styles, actions) over a bottom row of expandedHeight - collapsedHeight holding the expanded title (AppBarMediumFlexibleTokens / AppBarLargeFlexibleTokens styles), bottom-arranged with its last baseline titleBottomPadding above the bottom, reduced when it does not fit (TopAppBarMeasurePolicy). Scrolling collapses the bottom row only: Compose shrinks it (title moving up, clipped); here the box keeps its height and is clipped, the expanded title moved up by the offset. The container color, both title alphas and the semantics follow the collapsed fraction, without animation (m3e-motion twoRowsFrame).