Android preview
One declaration per app: the App Toolkit shell (app bars, bar, rail or drawer, insets, sheets) drawn around each marketing screen's content.
import 'material-next-web-components/marketing.js';On this page
Overview
Apps built on App Toolkit share one shell: the app bar and its search, the navigation the window calls for, the system bars and the sheets and drawers over the content. The Android preview draws that shell once, for every marketing UI, from a declaration of the app:
defineProductIdentity(config): the app's name, icons, fonts, palettes and destinations, declared once.identity.appis the shell declaration;identity.productis what the marketing hub shows. See Product identity.definePreviewApp(config): the shell declaration alone (name, mark, tabs, drawer entries and footer, overflow menu), for a preview without a hub.previewScreen(app, state, destination): what a screen renders. The destination gives its tab, title, app bar, content, FAB and overlay; nothing else.<mnw-android-screen>: the elementpreviewScreen()renders. Use it directly when you wire events yourself.
Which controls appear is decided by previewChrome(), never by a screen, following App Toolkit's ShellChrome.kt and ShellFrame.kt:
| Layout (width) | Navigation | App bar on a tab | Rail button |
|---|---|---|---|
| Bottom bar (< 600 dp) | Navigation bar, modal drawer | Menu opens the drawer | None |
| Rail (600 dp) | Collapsed rail | No menu | Expand: opens a modal rail |
| Expanded rail (840 dp) | Rail, collapsed or expanded | No menu | Expand or collapse |
| Permanent drawer (1200 dp) | Drawer with the tabs | No menu | None |
A destination is a tab's root, a child pushed onto the tab, or a page such as settings or help. A child keeps the navigation bar and marks no tab. A page covers the navigation bar on a phone and opens beside the rail or drawer on wider windows. Both show Back.
The app bar on a tab's root names the app until the navigation beside it names the app, as an expanded rail or the permanent drawer does. From then on it names the tab. A child or page names itself through title, which also overrides the shell's title anywhere. When the title changes, the new one fades in, and an expanding rail shows its name and labels once it has opened.
The overflow button appears on a tab's root when the app declares overflow entries, beside any actions the destination adds. An expanded rail stays expanded when another tab or entry opens.
Live demo
phone: navigation bar, menu, modal drawerfold: expandable rail, no menu in the app bardesktop: permanent drawer with the tabs<div class="frames-demo">
<figure>
<div data-window="phone"></div>
<figcaption><code>phone</code>: navigation bar, menu, modal drawer</figcaption>
</figure>
<figure>
<div data-window="fold"></div>
<figcaption><code>fold</code>: expandable rail, no menu in the app bar</figcaption>
</figure>
<figure>
<div data-window="desktop"></div>
<figcaption><code>desktop</code>: permanent drawer with the tabs</figcaption>
</figure>
</div>import {html} from 'lit';
import {
ScreenHost,
createScreenRegistry,
definePreviewApp,
previewScreen,
type MarketingScreen,
type PreviewScreenState,
} from 'material-next-web-components/marketing.js';
// One declaration for the whole app. Settings and Help are pages this preview does not draw.
const APP = definePreviewApp({
name: 'Sample',
tabs: [
{id: 'today', label: 'Today', icon: 'today'},
{id: 'tools', label: 'Tools', icon: 'grid_view'},
],
drawer: {
footer: [
{id: 'settings', label: 'Settings', icon: 'settings'},
{id: 'help', label: 'Help & feedback', icon: 'help_outline'},
],
},
});
// Each screen renders its content and says which tab it is; the shell does the rest.
const tab = (id: 'today' | 'tools', text: string): MarketingScreen => ({
id,
title: id,
description: text,
defaults: {sheet: false},
render: (s) => {
const state = s as PreviewScreenState;
return previewScreen(APP, state, {
tab: id,
content: html`<div style="padding: 16px; display: grid; gap: 12px">
<p style="margin: 0">${text}</p>
<button @click=${() => state.set?.({sheet: true})}>Open a sheet</button>
</div>`,
overlay: state.sheet
? {
presentation: 'sheet',
label: 'Details',
content: html`<p style="margin: 0 0 16px">
A sheet. Escape, the scrim or Back closes it.
</p>`,
onDismiss: () => state.set?.({sheet: false}),
}
: null,
});
},
});
const registry = createScreenRegistry([
tab('today', 'The bottom bar layout: the menu opens the drawer.'),
tab('tools', 'Beside a rail or a drawer, the app bar has no menu.'),
]);
export default (root: HTMLElement) => {
const hosts = [...root.querySelectorAll<HTMLElement>('[data-window]')].map(
(el) =>
new ScreenHost(el, {
registry,
id: 'today',
window: el.dataset.window,
frame: true,
onGo: (id, state, host) => host.load(id, state),
})
);
return () => hosts.forEach((h) => h.destroy());
};Usage
import {
defineProductIdentity,
previewScreen,
supportUs,
toolkitFooter,
} from 'material-next-web-components/marketing.js';
// src/config/identity.js: toolkitGraph(appTitle, appIcon) { tab(...); drawer { toolkitFooter() }; overflow { supportUs() } }
export const IDENTITY = defineProductIdentity({
id: 'app-toolkit',
name: 'App Toolkit',
icons: {app: '/logos/icon.svg', logo: '/logos/app_logo.svg'},
fonts: {brand: {family: 'Google Sans Flex', src: 'fonts/GoogleSansFlex.ttf', weight: '1 1000', stretch: '25% 151%'}},
colors: {palettes: ['captured', 'dynamic', 'blue'], defaultPalette: 'captured'},
destinations: {
tabs: [
{id: 'quick-tools', label: 'Quick Tools', icon: {name: 'grid_view', rounded: true}},
{id: 'numbers', label: 'Measure', icon: 'straighten'},
{id: 'apps', label: 'Apps', icon: 'apps'},
],
drawer: {footer: toolkitFooter()},
overflow: [supportUs()],
},
});
// In a MarketingScreen:
render(s) {
return previewScreen(IDENTITY.app, s, {
tab: 'numbers',
title: 'Measure',
content: html`...`,
overlay: s.sheet ? {presentation: 'sheet', label: 'Counter', content: html`...`, onDismiss: () => s.set({sheet: null})} : null,
});
}Icons are Material Symbols names. {name, rounded: true} draws one from Material Symbols Rounded, which the page must load (material-symbols/rounded.css) as it loads Outlined.
The drawer, rail and menu state lives in the screen's own state as drawerOpen, railExpanded, railOpen and overflowOpen, so a preset or a URL (state={"drawerOpen":true}) can open them. A tab opens its screen (default: its id) with go(). An entry without screen is a page the preview does not draw: it is drawn like the app draws it, carries data-preview-only and the title "Not part of this preview", and only closes the navigation.
Product identity
defineProductIdentity() is the one place an app names itself. Everything that draws the app reads it, so the app bar, the rail, the drawers, the previews and the marketing hub cannot disagree:
| Field | What reads it |
|---|---|
id |
The hub, to keep each app's saved preview settings apart. |
name, label |
name is the store's and the hub's. label is the name the app draws on itself (R.string.app_name): the app bar on a tab's root, the expanded rail, the drawer header. It defaults to name. |
icons |
app is the square launcher icon: the hub. logo is the mark beside the name inside the app (ShellGraph.appIcon): the drawer header; it defaults to app. monochrome is the one-colour variant for artwork that needs it. |
fonts |
brand and plain become --md-ref-typeface-brand and --md-ref-typeface-plain, which the shell, Material Web and the app's type scale read. With src, loadProductFonts() registers the face from the app's assets. symbols lists the icon fonts to wait for before a capture. |
colors |
palettes lists the app's data-palette values in picker order (captured keeps the page's own tokens); defaultPalette is where previews start; seed is the brand colour for anything outside the app. |
destinations |
tabs, drawer and overflow, as in definePreviewApp(). |
links |
privacy and codeOfConduct in the hub's drawer; repository for the app's own pages. |
Assets stay in the app. The identity holds their paths and resolves them through identity.asset(path) against a base the host can move with identity.setAssetBase(), as a Remotion composition does for staticFile(); the drawer's mark is resolved when it is drawn, so a base set after the identity loads still applies.
App Toolkit presets
toolkitFooter({updates?, screens?}) and supportUs({screen?}) are the App Toolkit library's own entries, as ToolkitDrawer.kt and overflow { supportUs() } declare them: Settings, Help & feedback, Updates (omit with updates: false when the app has no changelog) and Share, and Support the app. An app on App Toolkit declares only its tabs and its own entries; their pages are the library's, so they close the navigation unless the app maps one to a screen it draws.
Overlays
overlay.presentation is sheet (a ModalBottomSheet, opened straight to expanded over a 32% scrim), dialog (an AlertDialog centred over the scrim, 280 to 560 dp wide by its content) or fullscreen (a full-screen dialog over the status bar). handle: true gives a sheet its drag handle; flush: true leaves the padding to the content, and a flush sheet lays its content out as a column, so the content's own scroll area scrolls and its footer stays pinned. The shell owns what every overlay needs:
- It is drawn inside the screen and clipped by it: no popover or top-layer content escapes the device frame.
- The content behind it is
inert; focus moves into it when it opens and back to what opened it when it closes. - Escape, the system Back button and the scrim close the topmost layer: the overflow menu, then the drawer or modal rail, then the overlay (
onDismiss).
Scrolling, bottom bars and lists
bottomis the destination's own bottom bar (Scaffold'sbottomBar): an action row or a message field between the content and the navigation bar. Beside a rail it keeps clear of the system navigation.collapse(0 to 1) drives a large app bar for a screen that scrolls from its state, so a capture of any position is deterministic. Without it the bar follows the content's own scrolling, andonScrollreports the position (scrollTop,scrollHeight,clientHeight,collapseHeight) for a screen that follows it, such as reading progress.scrollPositionstarts the content scrolled, for a capture of a scrolled screen.titleLineslets a large title wrap before it ellipsises.hideNavigationBarhides the navigation bar on scroll (HideOnScrollBottomBar) and gives its inset back to the content.- A child drawn as the detail beside its tab's list on a wide window sets
besideList, and its tab stays marked, ashighlightedTabdoes. - Drawer entries take a
badge: a count, or''for the dot.preview.statusBar: 'blank'on the identity leaves the status bar empty, for timeless captures.
Captures and videos
previewScreen() sets interactive only when the state has set(). Without it (createMarketingEmbed(), Remotion) the shell renders the state it is given: no handlers, no entrance animations, overlays already settled. settle() waits for the shell's own Material Web parts inside its shadow root.
API
An app screen inside the App Toolkit shell, drawn as Compose draws it: status bar, app bar, the navigation the window's layout calls for (bar, rail, expanded rail or permanent drawer), the system navigation, and the modal drawer, modal rail, overflow menu and sheets over it.
The app declares itself once with definePreviewApp(); each destination sets its tab, title,
app bar and overlay. Which buttons appear comes from previewChrome(), never from the screen.
Content stays in light DOM, so the app's own CSS applies to it.
Without interactive (videos and captures) nothing reacts and nothing animates in: the shell
renders the state it is given. Interactive, it never changes its own state; it asks the host:
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
app | Property only | PreviewApp | undefined | ||
window | Property only | string | WindowSpec | undefined | A window name (phone, fold, desktop) or size. | |
navType | nav-type | 'gesture' | 'buttons' | 'gesture' | |
tab | tab | string | undefined | The tab this destination belongs to, selected in the navigation. | |
kind | kind | PreviewDestinationKind | 'root' | |
besideList | beside-list | boolean | false | A child open as the detail beside its tab's list: its tab stays marked in the navigation. |
heading | heading | string | undefined | The app bar title, overriding the shell's: a tab's root names the app, or the tab once the navigation beside it names the app; a child or page has no default. | |
topBar | top-bar | PreviewTopBar | 'small' | |
searchPlaceholder | search-placeholder | string | 'Search' | |
searchValue | search-value | string | '' | |
actions | Property only | PreviewAction[] | [] | |
navigation | Property only | PreviewNavigationState | {} | |
overlay | Property only | PreviewOverlay | null | null | |
scrolled | scrolled | boolean | false | The app bar and status bar take the scrolled colour. |
collapse | collapse | number | undefined | How far a large app bar has collapsed, 0 to 1, for screens that scroll from their state (captures). Unset, the bar follows the content's own scrolling. | |
titleLines | title-lines | number | 1 | A large title's lines before it ellipsises (an article's title wraps to two). |
scrollPosition | scroll-position | number | undefined | Where the content is scrolled to, for screens and captures that start scrolled. Applied when it changes; the person can scroll on from there. | |
hideNavigationBar | hide-navigation-bar | boolean | false | The navigation bar has hidden on scroll (HideOnScrollBottomBar): the content takes its place. |
interactive | interactive | boolean | false | Live preview: reacts to input and animates overlays in. Off in videos and captures. |
| Event | Description |
|---|---|
type | |
preview-navigation | {patch}: a PreviewNavigationState change (drawer, rail, menu). |
preview-tab | {tab}: a tab was chosen. |
preview-entry | {entry}: a drawer, rail or overflow entry was chosen. |
preview-back | The app bar's or the system's Back. |
preview-search | {value}: the tab's search text changed. |
preview-action | {id}: an app bar action was pressed. |
preview-dismiss | The overlay was dismissed: scrim, Escape or Back. |
preview-scroll | {scrollTop, scrollHeight, clientHeight, collapseHeight}: the content scrolled. |
| Slot | Description |
|---|---|
| Default | The destination's content. |
fab | A floating action button, placed above the navigation bar and the system bars. |
bottom | The destination's own bottom bar (Scaffold's bottomBar): an action row or a message field, between the content and the navigation bar. |
top | Replaces the app bar, for a destination with its own (an article's). |
overlay | The overlay's content (see overlay). |
Accessibility
- Navigation buttons are labelled "Open navigation drawer", "Expand navigation", "Collapse navigation" and "Navigate up"; the rail button reports
aria-expanded. - The drawer, modal rail and overlays are
role="dialog"witharia-modal; the overflow menu isrole="menu". - The selected tab and drawer destination carry
aria-current="page".
Limitations
- List-detail panes and the scrolling behaviours of large app bars beyond collapse are left to the app's content.
- The system Home and Recents buttons are drawn but do nothing.