Product launcher
An apps button that opens favorite products, consoles and shortcuts, with admin redirects in their own section, editable favorites and local persistence.
import 'material-next-web-components/launcher.js';On this page
Overview
<mnw-product-launcher> is the apps button of a product's app bar. It opens a panel with two parts:
- Your favorites: the visitor's own list of Android apps, web products, API consoles and external shortcuts. It is editable: add, remove, reorder, reset.
- Admin redirects: a fixed list of consoles (Play Console, Firebase, AdMob, Cloudflare). It stays its own section; while editing, a redirect can be copied into favorites.
Favorites persist in localStorage under one key, so every product served from the same origin shares them. The panel never offers the page you are on: entries matching current-product, exclude or this page's URL are hidden. A hidden favorite keeps its place for the pages where it shows.
Without a catalog, the launcher's favorites are the Android Insider and Apps Metadata admin consoles, GitHub, the developer’s Google Play account and Material Next, alongside the admin redirects from android-apps-metadata-backend. This site's app bar uses it, with current-product="web:material-next".
Live demo
<label class="demo-launcher__palette">
<md-checkbox id="launcher-palette" touch-target="wrapper"></md-checkbox>
Follow color palette
</label>
<div class="demo-launcher">
<span class="demo-launcher__label">Your projects, on the Material Next site</span>
<mnw-product-launcher id="projects-launcher" current-product="web:material-next" storage-key="mnw-docs-demo:launcher"></mnw-product-launcher>
</div>
<div class="demo-launcher">
<span class="demo-launcher__label">A custom catalog, on Pocket Notes' own page</span>
<mnw-product-launcher id="custom-launcher" current-product="pocket-notes" storage-key="mnw-docs-demo:custom-launcher" redirects-heading="Consoles"></mnw-product-launcher>
</div>
<pre class="demo-log" aria-live="polite"></pre>import {createLauncherCatalog} from '../../launcher/index.js';
import '@material/web/checkbox/checkbox.js';
import type {MdCheckbox} from '@material/web/checkbox/checkbox.js';
import type {MaterialNextProductLauncher} from '../../launcher/index.js';
// A catalog of your own: an app, its site, its console and two shortcuts.
// Pocket Notes' app and site share a product, so on Pocket Notes' pages
// (current-product="pocket-notes") neither is offered.
const catalog = createLauncherCatalog({
items: [
{
kind: 'android-app',
key: 'pocket-notes',
title: 'Pocket Notes',
subtitle: 'Android app',
href: 'https://play.google.com/store/apps/details?id=com.example.pocketnotes',
icon: 'sticky_note_2',
product: 'pocket-notes',
},
{
kind: 'web',
key: 'pocket-notes',
title: 'Pocket Notes site',
href: 'https://example.com/pocket-notes/',
icon: 'public',
product: 'pocket-notes',
},
{
kind: 'shortcut',
key: 'issues',
title: 'Issues',
href: 'https://github.com/MihaiCristianCondrea/material-next-web-components/issues',
icon: 'bug_report',
},
// A duplicate URL: dropped, the first entry wins.
{
kind: 'shortcut',
key: 'issues-again',
title: 'Issues (again)',
href: 'https://github.com/MihaiCristianCondrea/material-next-web-components/issues/',
},
{
kind: 'console',
key: 'play-console',
title: 'Play Console',
href: 'https://play.google.com/console/',
image: 'asset:launcher/play-console.svg',
},
{
kind: 'console',
key: 'firebase',
title: 'Firebase',
href: 'https://console.firebase.google.com/',
image: 'asset:launcher/firebase.svg',
},
],
defaultFavorites: ['android-app:pocket-notes', 'shortcut:issues'],
redirects: ['console:play-console', 'console:firebase'],
});
export default (root: HTMLElement) => {
const custom =
root.querySelector<MaterialNextProductLauncher>('#custom-launcher')!;
const log = root.querySelector('.demo-log')!;
const palette = root.querySelector<MdCheckbox>('#launcher-palette')!;
const onPalette = () => {
for (const launcher of root.querySelectorAll<MaterialNextProductLauncher>(
'mnw-product-launcher'
)) {
launcher.followColorPalette = palette.checked;
}
};
palette.addEventListener('change', onPalette);
custom.catalog = catalog;
const onChange = (event: Event) => {
const {favoriteIds} = (event as CustomEvent<{favoriteIds: string[]}>)
.detail;
log.textContent = `launcher-change ${JSON.stringify(favoriteIds)}`;
};
const onToggle = (event: Event) => {
const {open} = (event as CustomEvent<{open: boolean}>).detail;
log.textContent = `launcher-toggle {"open":${open}}`;
};
root.addEventListener('launcher-change', onChange);
root.addEventListener('launcher-toggle', onToggle);
log.textContent = 'Open a launcher: the apps button on the right.';
return () => {
palette.removeEventListener('change', onPalette);
root.removeEventListener('launcher-change', onChange);
root.removeEventListener('launcher-toggle', onToggle);
root.querySelectorAll('mnw-product-launcher').forEach((l) => l.close());
};
};Colors
The default is the Google launcher palette: a pale blue #E9EEF6 panel, a
#F8FAFD favorites card, and filled hover surfaces without tile outlines.
The host’s color-scheme selects Google light or dark surfaces. Artwork keeps its original colors. Turn on Follow color palette in the demo,
or add the boolean follow-color-palette attribute, to use the host’s Material
color roles instead. It is off by default.
<mnw-product-launcher follow-color-palette></mnw-product-launcher>There is one Android Insider destination, the admin console. The Google Play icon opens the developer account, rather than an individual app listing. Existing favorites pointing at the old Android Insider app entry migrate to this developer link.
Usage
Import the module and place the element in an app bar's trailing area:
<script type="module">
import 'material-next-web-components/launcher.js';
</script>
<mnw-product-launcher
current-product="android-app:pocket-notes"
></mnw-product-launcher>The project icons ship in assets/marketing/launcher/. Copy them with npx mnw-marketing-assets public/material-next, or point configureMarketingAssets({base}) at where you serve them. An entry whose image fails to load shows its Material Symbol instead.
Dark mode
The panel, favorites card, hover surfaces, edit controls and monochrome artwork follow the host's
color-scheme. This also works while the panel is open. Leave Follow color palette off for
Google's light and dark colors, or turn it on to use the host's Material roles.
Material Next's applyColorTheme() sets the scheme for light, dark and system modes. If a host owns
its theme, set color-scheme: light, dark or light dark on its root along with its color tokens.
Icon artwork
The launcher draws each tile and its hover surface; an entry's image is only the mark. It is contained in a 40px square, so every icon reads at the same size whatever its proportions:
- Format: SVG first, then PNG or WebP (at least 80px for sharp 2x screens).
- No frame of its own: a transparent background, with no border, tile, circle or drop shadow. A logo that comes inside a rounded square looks smaller than its neighbours and doubles the tile.
- Cropped close: little empty margin around the artwork. A wide logo keeps its proportions and is fitted to the square's width.
- One colour that inverts: set
monochromefor a single dark mark (GitHub's) so it turns light in dark themes.
Entries and ids
Every entry has a kind and a key, and its id is kind:key:
| Kind | For | Example id |
|---|---|---|
android-app |
An app's Play listing | android-app:pocket-notes |
web |
A product site or web app | web:material-next |
console |
An admin console or API dashboard | console:play-console |
shortcut |
Anything else: a profile, a tool, an issue tracker | shortcut:github |
The kind is part of the id, so an app and its site can share a key (android-app:pocket-notes, web:pocket-notes). Keys use lowercase letters, digits, dots and dashes; launcherItem() rejects anything else. Stored favorites refer to ids only, so titles, URLs and icons can change without losing anyone's list.
Your own catalog
import {createLauncherCatalog} from 'material-next-web-components/launcher.js';
launcher.catalog = createLauncherCatalog({
items: [
{
kind: 'android-app',
key: 'pocket-notes',
title: 'Pocket Notes',
href: 'https://play.google.com/store/apps/details?id=com.example.notes',
icon: 'sticky_note_2',
product: 'pocket-notes',
},
{
kind: 'web',
key: 'pocket-notes',
title: 'Pocket Notes site',
href: 'https://example.com/notes/',
product: 'pocket-notes',
},
{
kind: 'console',
key: 'play-console',
title: 'Play Console',
href: 'https://play.google.com/console/',
image: 'asset:launcher/play-console.svg',
},
],
defaultFavorites: ['android-app:pocket-notes'],
redirects: ['console:play-console'],
});createLauncherCatalog() deduplicates: a second entry with the same id or the same URL (ignoring query, hash and a trailing slash) is dropped, and the first one wins. Default favorites and redirects that name no entry are dropped too.
Which entries a page shows
An entry is hidden when any of these match it:
current-product: an item id (web:material-next) or aproductname shared by several entries (pocket-noteshides its app and its site);exclude: more ids or product names, separated by spaces or commas;- the page URL (
current-url, orlocation.href): an entry linking to this page, or forwebandconsoleentries, to a page above it.
Editing
Choose Edit to start a temporary draft. Done saves the draft and fires launcher-change once.
Cancel, Escape, clicking outside or closing the panel discard it. Other tabs continue to see
saved favorites until Done. Select an app to add or remove it; drag within Favorites to reorder,
or between Favorites and More apps to add or remove. A lifted preview follows the pointer while the other tiles move into place; dropping settles it into its slot. Entering and leaving Edit mode animates the panel and controls. Alt+arrow keys reorder, and Delete removes.
Reset to defaults also stays temporary until Done.
Storage
createLauncherPreferenceStore() keeps {"version": 1, "favoriteIds": [...]} under storage-key (default mnw-launcher:favorites). It also reads a bare list and the older {favoriteIds} shape, turns the backend launcher's ids (github-profile) into this catalog's (shortcut:github), and keeps working in memory when storage is full, blocked or corrupt. Another tab's change updates the open launcher outside edit mode. Cancel reloads the latest saved list; Done saves the current draft. To keep favorites somewhere else, for example in an account, pass your own store with the same four methods.
API
An apps button that opens the visitor's favorite products, consoles and shortcuts, with a separate list of admin redirects. Visitors edit their favorites in a draft (select to add or remove, drag or Alt+arrow to reorder). Done saves; Cancel, Escape and closing discard the draft. The list persists in localStorage, shared by every page on the origin.
The current product is never offered: entries matching current-product,
exclude or this page's URL are hidden, and a hidden favorite keeps its
place for the pages where it shows. Without a catalog the launcher lists
Mihai-Cristian Condrea's projects (PROJECT_LAUNCHER_CATALOG).
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
catalog | Property only | LauncherCatalog | PROJECT_LAUNCHER_CATALOG | Everything the launcher can show. Defaults to PROJECT_LAUNCHER_CATALOG. |
currentProduct | current-product | string | '' | The product this page belongs to: an item id or a product name. |
currentUrl | current-url | string | '' | This page's URL, for hiding links to itself. Defaults to location.href. |
exclude | exclude | string[] | [] | More item ids or product names to hide, separated by spaces or commas. |
storageKey | storage-key | DEFAULT_LAUNCHER_STORAGE_KEY | The localStorage key favorites persist under. | |
store | Property only | LauncherPreferenceStore | undefined | Replaces localStorage, for example with a store that syncs to an account. | |
open | open | boolean | false | The panel is open. |
followColorPalette | follow-color-palette | boolean | false | Use the host's Material color roles instead of the Google launcher palette. |
label | label | string | 'Products and shortcuts' | Accessible name of the apps button. |
heading | heading | string | 'Your favorites' | Title of the favorites card. |
redirectsHeading | redirects-heading | string | '' | Optional visible heading above admin redirects; empty by default. |
| Event | Description |
|---|---|
launcher-toggle | {open}: the panel opened or closed. |
launcher-change | {favoriteIds}: Done or reset committed favorites (hidden ones included). |
| Custom property | Description |
|---|---|
--mnw-launcher-width | Panel width from 600px up. Default 360px. |
--mnw-launcher-monochrome-filter | Filter for monochrome icons. Default none; monochrome artwork follows the panel text color. |
| Part | Description |
|---|---|
trigger | The apps button. |
panel | The panel surface. |
Methods: show(), close(), toggle() and reset(); favoriteIds returns the current list (the draft while editing), hidden entries included. Calling reset() directly commits the defaults immediately and forgets storage.
Module exports
Everything below comes from material-next-web-components/launcher.js; launcher/<path>.js imports one layer.
| Layer | Exports |
|---|---|
| Domain: entries | launcherItem, launcherItemId, isLauncherItemId, normalizeHref, LAUNCHER_ITEM_KINDS, types LauncherItem, LauncherItemInput, LauncherItemId, LauncherItemKind |
| Domain: catalog | createLauncherCatalog, findLauncherItem, normalizeFavoriteIds, MAX_FAVORITES (48), type LauncherCatalog |
| Domain: favorites | resolveLauncher (visible favorites, redirects, available entries, hidden favorite ids), isExcluded, addFavorite, removeFavorite, moveFavorite, types LauncherContext, LauncherView |
| Data | createLauncherPreferenceStore, DEFAULT_LAUNCHER_STORAGE_KEY, types LauncherPreferenceStore, KeyValueStorage |
| Catalog | PROJECT_LAUNCHER_CATALOG, PROJECT_LAUNCHER_ITEMS, migrateLegacyLauncherId |
| UI | MaterialNextProductLauncher (<mnw-product-launcher>), event types LauncherChangeEvent, LauncherToggleEvent |
The domain functions are pure, so a server-rendered page or a test can work out what a launcher shows without a browser.
The former Tutorials admin entry is named Android Insider Admin Console, preserving its saved ID. The built-in catalog does not offer Codex or Smart Cleaner. Existing stored IDs are preserved for compatibility with other catalogs but do not appear here. Hover highlights the tile surface without reshaping the icon. Edit mode uses a still grid with Cancel and Done above the apps.
The panel follows the Google apps launcher: a single rounded favorites surface, a plain shortcuts grid underneath, and no favorites/redirects counts. redirects-heading is empty by default; set it to show an optional section label.
Accessibility
- The apps button reports
aria-expanded; the panel is a labelled dialog that takes focus when it opens and returns it to the button when Escape, the close button or a tap on the scrim closes it. A click outside or tabbing past the panel closes it too. - Edit moves focus to Cancel. Each favorite is a button named with its remove action and position, described by the reordering instructions. Done and Cancel return focus to Edit.
- Moves, additions, removals and resets are announced in a polite live region; focus stays on the moved tile or goes to its neighbour after a removal.
- Links that open a new tab say so to screen readers. Tiles are at least 96px tall. The drag preview is inert and hidden from accessibility tools.
- On phones (below 600px) the panel is a bottom sheet with a close button. Reduced motion disables opening, mode-change, reorder and drop animations. Closing or disconnecting clears active motion.
Limitations
- The panel is placed under the button, aligned to its end edge; it suits app bars, not buttons near the start of the window.
- Favorites live on one device and origin unless you pass a
storethat syncs them. - Favorites can be reordered or dragged to More apps to remove them. Entries in More apps can be dragged into Favorites or selected to add them. Changes remain temporary until Done.