Skip to content

Navigation

Docked toolbar

A toolbar docked to the bottom of the window for actions on the current page.

Examples

Arrangements

Spread (the default), center (shown vibrant), space-around and space-evenly. Dock the toolbar yourself at the bottom of the screen or of a container.

The live demo runs with JavaScript. Its code is below.

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

const ACTIONS = [
  ['arrow_back', 'Back'],
  ['bookmark', 'Bookmark'],
  ['share', 'Share'],
  ['more_vert', 'More options'],
] as const;

export default function Arrangements() {
  return (
    <div style={{ display: 'grid', gap: 'var(--m3e-sys-space-200)', inlineSize: '100%' }}>
      {(['spread', 'center', 'space-around', 'space-evenly'] as const).map((arrangement) => (
        <DockedToolbar
          key={arrangement}
          aria-label={`Actions, ${arrangement}`}
          arrangement={arrangement}
          colorStyle={arrangement === 'center' ? 'vibrant' : 'standard'}
        >
          {ACTIONS.map(([icon, label]) => (
            <IconButton key={icon} aria-label={label}>
              <Icon name={icon} />
            </IconButton>
          ))}
        </DockedToolbar>
      ))}
    </div>
  );
}
<div style="display: grid; gap: var(--m3e-sys-space-200); inline-size: 100%">
  <m3e-docked-toolbar label="Actions, spread" arrangement="spread">
    <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>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Bookmark">
      <span class="m3e-icon" aria-hidden="true">bookmark</span>
    </button>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Share">
      <span class="m3e-icon" aria-hidden="true">share</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>
  </m3e-docked-toolbar>
  <m3e-docked-toolbar label="Actions, center" arrangement="center" color-style="vibrant">
    <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>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Bookmark">
      <span class="m3e-icon" aria-hidden="true">bookmark</span>
    </button>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Share">
      <span class="m3e-icon" aria-hidden="true">share</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>
  </m3e-docked-toolbar>
  <m3e-docked-toolbar label="Actions, space-around" arrangement="space-around">
    <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>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Bookmark">
      <span class="m3e-icon" aria-hidden="true">bookmark</span>
    </button>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Share">
      <span class="m3e-icon" aria-hidden="true">share</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>
  </m3e-docked-toolbar>
  <m3e-docked-toolbar label="Actions, space-evenly" arrangement="space-evenly">
    <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>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Bookmark">
      <span class="m3e-icon" aria-hidden="true">bookmark</span>
    </button>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Share">
      <span class="m3e-icon" aria-hidden="true">share</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>
  </m3e-docked-toolbar>
</div>

Run the HTML version (custom elements, no framework).

Bottom app bar (deprecated)

Deprecated (D4): BottomAppBar ships until 2.0. Use a docked toolbar, with a FAB or a floating toolbar for the main action.

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

/** Deprecated (D4): use DockedToolbar, with a FloatingToolbar or FAB for the main action. */
export default function LegacyBottomAppBar() {
  return (
    <BottomAppBar
      aria-label="Actions"
      style={{ inlineSize: '100%' }}
      actions={
        <>
          <IconButton aria-label="Done">
            <Icon name="check" />
          </IconButton>
          <IconButton aria-label="Edit">
            <Icon name="edit" />
          </IconButton>
          <IconButton aria-label="Delete">
            <Icon name="delete" />
          </IconButton>
        </>
      }
      floatingActionButton={<Fab icon={<Icon name="add" />} aria-label="Add" />}
    />
  );
}
<!-- Deprecated (D4): use a docked toolbar. The CSS classes have no roving focus; m3e-react's
     <BottomAppBar> adds it. -->
<div class="m3e-bottom-app-bar" role="toolbar" aria-label="Actions" style="inline-size: 100%">
  <div class="m3e-bottom-app-bar__actions">
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Done">
      <span class="m3e-icon" aria-hidden="true">check</span>
    </button>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Edit">
      <span class="m3e-icon" aria-hidden="true">edit</span>
    </button>
    <button type="button" class="m3e-icon-button m3e-icon-button--standard" aria-label="Delete">
      <span class="m3e-icon" aria-hidden="true">delete</span>
    </button>
  </div>
  <div class="m3e-bottom-app-bar__fab">
    <button type="button" class="m3e-fab" aria-label="Add">
      <span class="m3e-icon" aria-hidden="true">add</span>
    </button>
  </div>
</div>

Run the HTML version (CSS classes only).

API

React

<DockedToolbar>

Docked toolbar (COMPONENT_SPEC §10; Compose FlexibleBottomAppBar): the .m3e-docked-toolbar contract with role="toolbar". Roving focus and the exit-always scroll behavior come from m3e-elements' DockedToolbarLayout, the same code <m3e-docked-toolbar> runs. Dock it at the bottom of the screen or of a container yourself (position fixed / sticky / absolute).

Prop Type Default Description
children ReactNode The toolbar items, usually <IconButton>s (each takes a 48dp slot).
arrangement "spread" | "center" | "space-around" | "space-evenly" spread Default spread.
colorStyle "standard" | "vibrant" standard Default standard.
scrollBehavior "exit-always" | "exitAlways" Default: none (static). 'exitAlways' is deprecated: use 'exit-always'.
scrollTarget RefObject<HTMLElement | null> The scrolling element the toolbar is docked under. Default: the document.
aria-label string The toolbar's accessible name (or use aria-labelledby).

Also accepts HTML attributes not listed here.

<BottomAppBar>

Deprecated. D4: use DockedToolbar. Shipped until 2.0; no new features.

Legacy bottom app bar (Compose BottomAppBar with actions and floatingActionButton): 80dp tall, actions from the start, the FAB at the end. role="toolbar" with the docked toolbar's roving focus and exit-always scroll behavior.

Prop Type Default Description
actions ReactNode The actions, usually <IconButton>s (each takes a 48dp slot), from the start.
floatingActionButton ReactNode A FAB at the end, usually <Fab>: colored secondary container with no elevation, as Compose's BottomAppBarWithFAB sample (bottomAppBarFabColor, bottomAppBarFabElevation()).
scrollBehavior "exit-always" | "exitAlways" Default: none (static).
scrollTarget RefObject<HTMLElement | null> The scrolling element the bar is docked under. Default: the document.
aria-label string The bar's accessible name (or use aria-labelledby).

Also accepts HTML attributes not listed here.

dockedToolbarClassName()

Classes for the .m3e-docked-toolbar root (the m3e-css contract).

Custom elements

<m3e-docked-toolbar>

import '@kyralabs/m3e-elements/docked-toolbar.js';

<m3e-docked-toolbar label="Actions"> (COMPONENT_SPEC §10; Compose FlexibleBottomAppBar). Renders the m3e-css .m3e-docked-toolbar contract inside itself (the host is display: contents) and moves its children (usually .m3e-icon-buttons) into it.

  • Attributes: arrangement (spread default | center | space-around | space-evenly), color-style (standard | vibrant), label (the toolbar's accessible name), scroll-behavior (exit-always), scroll-target (id of the scrolling element; default the document).
  • Behavior (m3e-elements DockedToolbarLayout, shared with m3e-react): role="toolbar" with roving focus (one tab stop, ArrowLeft / ArrowRight, Home / End) and the exit-always scroll behavior (hides on scroll down, returns on scroll up, snaps on the fast spatial spring).
Attribute Property Type Notes
arrangement arrangement string
color-style colorStyle string
label label string
scroll-behavior scrollBehavior string
scroll-target scrollTarget string

CSS

docked-toolbar.css

@import '@kyralabs/m3e-css/docked-toolbar.css';

Docked toolbar (COMPONENT_SPEC §10; Compose FlexibleBottomAppBar). Static layout, the four arrangements, both color styles and the exit-on-scroll offset. <m3e-docked-toolbar> and m3e-react <DockedToolbar> (m3e-elements DockedToolbarLayout) add roving focus and the exit-always scroll behavior on the same classes.

<div class="m3e-docked-toolbar" role="toolbar" aria-label="Actions">…icon buttons…</div>

Modifiers: --center (fixed spacing, centered), --space-around, --space-evenly, --vibrant. Without a modifier the items spread (space between). Dock it yourself at the bottom of the screen or of a container (position: fixed / sticky / absolute, inset-block-end: 0).

12 custom properties it reads
Custom property Default
--m3e-comp-bottom-app-bar-container-shape var(--m3e-sys-shape-corner-none)
--m3e-comp-docked-toolbar-defaults-container-color var(--m3e-sys-color-surface-container)
--m3e-comp-docked-toolbar-defaults-container-height 64px
--m3e-comp-docked-toolbar-defaults-content-color var(--m3e-sys-color-on-surface)
--m3e-comp-docked-toolbar-defaults-content-padding-end 16px
--m3e-comp-docked-toolbar-defaults-fixed-horizontal-arrangement center
--m3e-comp-docked-toolbar-defaults-fixed-item-spacing 32px
--m3e-comp-docked-toolbar-defaults-horizontal-arrangement space-between
--m3e-comp-docked-toolbar-defaults-space-around-arrangement space-around
--m3e-comp-docked-toolbar-defaults-space-evenly-arrangement space-evenly
--m3e-comp-docked-toolbar-defaults-vibrant-content-color var(--m3e-sys-color-on-primary-container)
--m3e-comp-minimum-interactive-component-size 48px

bottom-app-bar.css

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

@deprecated (D4) Legacy bottom app bar (COMPONENT_SPEC §10; Compose BottomAppBar). Use the docked toolbar (docked-toolbar.css). Shipped until 2.0; no new features.

<div class="m3e-bottom-app-bar" role="toolbar" aria-label="Actions">
  <div class="m3e-bottom-app-bar__actions">…icon buttons…</div>
  <div class="m3e-bottom-app-bar__fab"><button class="m3e-fab" aria-label="Add">…</button></div>
</div>

m3e-react <BottomAppBar> (m3e-elements DockedToolbarLayout) adds roving focus and the exit-always scroll behavior on the same classes.

14 custom properties it reads
Custom property Default
--m3e-comp-bottom-app-bar-container-color var(--m3e-sys-color-surface-container)
--m3e-comp-bottom-app-bar-container-shape var(--m3e-sys-shape-corner-none)
--m3e-comp-bottom-app-bar-defaults-container-height 80px
--m3e-comp-bottom-app-bar-defaults-container-shadow var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-bottom-app-bar-defaults-content-padding-horizontal 4px
--m3e-comp-bottom-app-bar-defaults-content-padding-top 4px
--m3e-comp-bottom-app-bar-defaults-fab-container-color var(--m3e-sys-color-secondary-container)
--m3e-comp-bottom-app-bar-defaults-fab-content-color var(--m3e-sys-color-on-secondary-container)
--m3e-comp-bottom-app-bar-defaults-fab-padding-end 12px
--m3e-comp-bottom-app-bar-defaults-fab-padding-top 8px
--m3e-comp-bottom-app-bar-defaults-fab-shadow var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-bottom-app-bar-defaults-horizontal-arrangement flex-start
--m3e-comp-docked-toolbar-defaults-content-color var(--m3e-sys-color-on-surface)
--m3e-comp-minimum-interactive-component-size 48px

Accessibility

role="toolbar" with an aria-label; roving tabindex (one tab stop), ArrowLeft/Right (mirrored in RTL), Home/End, no wrapping (m3e-elements RovingFocus, as §9). The legacy bar is a toolbar over its actions and FAB.

the bar stays in the tab order while hidden (Compose has no focus rule for it); focus moving into a hidden bar brings it back, and it does not hide while it holds focus (WCAG 2.4.11).

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

no touch-exploration (TalkBack) detection, so the scroll behavior stays on for screen readers (Compose turns it off); no fling continuation (the browser's own momentum scrolls the content, as §9/§11); no dragging the bar itself (Compose's appBarDragModifier); the bar translates instead of shrinking its layout height, so content under a static (non-overlaid) bar does not gain the space; the samples' 56dp-wide FilledIconButton is shown with the wide icon button width (52dp) in the docs.

Sources

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

AppBar.kt (vendored), DockedToolbarTokens.kt, BottomAppBarTokens.kt, AppBarTokens.kt, AppBarSamples.kt (vendored), FloatingActionButton.kt (bottomAppBarFabElevation), ColorScheme.kt (contentColorFor).

DockedToolbarTokens.kt / BottomAppBarTokens.kt@19991f2c plus the BottomAppBarDefaults values Compose renders (components/docked-toolbar-defaults.css, components/bottom-app-bar-defaults.css; SOURCES register #28). Supersedes the earlier TODO(verify) list: height 64, no radius and no elevation are confirmed; "16 end padding" is 16 at both ends; "item spacing max 32 / min 4" is 32 for the center arrangement only, and the min 4 is not rendered.

docked-toolbar.css sources

(compose/material3 AppBar.kt and samples/AppBarSamples.kt @ 19991f2c; values in docked-toolbar-defaults.css, lines in PARITY.md):

  • Layout (FlexibleBottomAppBar L1286-1315 → BottomAppBarLayout L1318-1389): a Surface with BottomAppBarTokens.ContainerShape (CornerNone) and AppBarTokens.ContainerElevation (Level0, tonal: no shadow) holding a Row that is FlexibleBottomAppBarHeight (64dp) tall above the window insets (here the safe-area insets: horizontal + bottom), padded by FlexibleContentPadding (16dp start and end), items centered vertically. Items are IconButtons whose minimumInteractiveComponentSize reserves a 48dp slot: ours lay out at their visual size (touch target via ::after), so the bar adds the difference as margin, as the floating toolbar does.
  • Arrangements: FlexibleHorizontalArrangement = SpaceBetween (default, “spread”: the first and last items keep the 16dp padding); FlexibleFixedHorizontalArrangement = spacedBy( DockedToolbarTokens.ContainerMaxSpacing 32dp, CenterHorizontally) (“center”); the samples' SpaceAround / SpaceEvenly with no horizontal padding. DockedToolbarTokens.ContainerMinSpacing (4dp) is not read by Compose: spread items can meet.
  • Colors: BottomAppBarDefaults.containerColor (surfaceContainer) / contentColorFor (onSurface); vibrant: the ExitAlwaysBottomAppBarFixedVibrant sample's primaryContainer / onPrimaryContainer. Items inherit the content color (LocalContentColor), so standard icon buttons take it.
  • Exit on scroll (BottomAppBarDefaults.exitAlwaysScrollBehavior): the JS layers write the offset (0 → minus the bar height) and the bar moves down by it (Compose shrinks the bar's layout height by it, drawing the bar from its top; here the layout box stays put).
bottom-app-bar.css sources

(compose/material3 AppBar.kt @ 19991f2c; values in bottom-app-bar-defaults.css and docked-toolbar-defaults.css, lines in PARITY.md):

  • BottomAppBar (actions + floatingActionButton, L1077-1112 → L1194-1216 → BottomAppBarLayout L1318-1389): a Row BottomAppBarTokens.ContainerHeight (80dp) tall above the window insets (here the safe-area insets: horizontal + bottom), padded by ContentPadding (4dp start, top and end: 16dp minus IconButton's 12dp touch-target expansion), items centered vertically. The actions take the remaining width, from the start; items are IconButtons in 48dp slots (ours add the difference to their visual size as margin). The FAB slot is full height with 8dp above and 12dp after the FAB (FABVerticalPadding / FABHorizontalPadding), the FAB at its top start.
  • Container BottomAppBarDefaults.containerColor (surfaceContainer), content contentColorFor (onSurface), BottomAppBarTokens.ContainerShape (CornerNone), BottomAppBarDefaults .ContainerElevation 0dp (tonal; BottomAppBarTokens.ContainerElevation Level2 is not rendered).
  • The FAB as the BottomAppBarWithFAB sample passes it: BottomAppBarDefaults.bottomAppBarFabColor (secondaryContainer / onSecondaryContainer) with FloatingActionButtonDefaults .bottomAppBarFabElevation() (0dp in every state).
  • Exit on scroll (BottomAppBarDefaults.exitAlwaysScrollBehavior): as the docked toolbar.