# metroweb
Windows Phone 8's Metro design for React: the pivot, the panorama, toggle switches, pickers, the app bar and message boxes; the turnstile, feather and continuum page transitions and the tilt on press; the type ramp, the light and dark themes and the 20 accents. The values come from the phone wherever they could: its theme files and compiled markup, the Windows Phone Toolkit's source, and frame-by-frame measurements of real phones.
- **See it:** [wp8.vercel.app](https://wp8.vercel.app) is a whole phone built with it.
- **The spec:** [wp8.vercel.app/design](https://wp8.vercel.app/design.md) says what each component does, which value the code uses and how sure we are of it, with live demos.
- **Start an app:** copy the [starter template](https://github.com/ImperialBebek/metroweb/tree/main/templates/starter).
- **With a coding agent:** `npx skills add ImperialBebek/metroweb` installs the metro-ui skill: the steps, the rules and a checker for them. The docs are Markdown too, listed at [wp8.vercel.app/llms.txt](https://wp8.vercel.app/llms.txt).
## Install
```sh
npm install metroweb
```
It needs React 19 (`react` and `react-dom` are peer dependencies) and a bundler that imports CSS, such as Vite. The package is ES modules with TypeScript types.
## Quick start
```tsx
// main.tsx
import { createRoot } from 'react-dom/client'
import 'metroweb/styles.css'
import { App } from './App'
createRoot(document.getElementById('root')!).render()
```
```tsx
// App.tsx
import { useState } from 'react'
import { ListItem, Paragraph, PhoneFrame, Pivot, SettingsPage, ToggleSwitch, useNavigation } from 'metroweb'
function Home() {
const { navigate } = useNavigation()
return (
navigate('sync')} />
),
},
{ key: 'about', header: 'about', content: Built with metroweb. },
]}
/>
)
}
function Sync() {
const [on, setOn] = useState(true)
return (
)
}
export function App() {
return (route === 'sync' ? : )} />
}
```
`PhoneFrame` is the root of an app, as `PhoneApplicationFrame` is on the phone. It fills its container (`styles.css` makes `html`, `body` and `#root` full height), draws the phone's 480 px logical screen, scaled down when the container is narrower, and holds the page stack, the layer that popups open in, and the navigation bar. Escape, and Backspace outside a text field, work as the Back key.
## Pages and navigation
- `render(route)` returns the page for a route. Routes are strings your app parses itself, such as `'note:42'`.
- In a page, `useNavigation()` gives `navigate(route)`, `back()` and `registerBackHandler(handler)`. Back on the first page does nothing unless you pass `onExit`.
- Pages under the top one stay mounted and hidden, so Back returns to a page as it was left: its scroll position, pivot item and state.
- Pages use the feather transition: a page's `data-feather` elements (list items, paragraphs and the titles already are) turn in and out one after another from the top, with the tapped item leaving last. Pass `transition="turnstile"` to turn whole pages both ways instead. The [authoring contract](https://wp8.vercel.app/design/navigation.md#authoring-contract) lists the attributes, including continuum and `data-drop-out`.
A page with an app bar is an `.app-page`, with its content in an `.app-page-body` and the `AppBar` last:
```tsx
function NotePage({ onDelete }: { onDelete: () => void }) {
const { back, navigate } = useNavigation()
const [confirming, setConfirming] = useState(false)
return (
)
}
```
The message box, the pickers and the expanded app bar register for Back themselves, so Back closes them before it leaves the page.
## Components
| Export | What it is | On the phone |
|---|---|---|
| `PhoneFrame` | The app's root: the screen, page stack, popup layer and navigation bar | `PhoneApplicationFrame` |
| `PageStack`, `useNavigation` | The page stack on its own, for a shell of your own | `NavigationService` |
| `NavigationBar` | The Back, Start and Search keys | 8.1 on-screen navigation bar |
| `Pivot` | Pages under one title, switched by swiping or tapping the headers | UIX `Pivot` |
| `SettingsPage` | A single-page pivot: the app's title and one page header | UIX `SinglePivotPage` |
| `Panorama` | Wide sections under a big title and a background picture, both moving slower than the sections; wraps after the last | UIX `Panorama` |
| `ListItem` | A list row with a title and an optional subtitle; put rows in a `.list` | UIX `DoubleLineListItem` |
| `Paragraph` | Body text, normal or subtle | `PhoneTextNormalStyle` |
| `Button` | The outlined button | `PhoneButtonBase` |
| `ToggleSwitch` | An on/off switch under a header | Toolkit `ToggleSwitch` |
| `ListPicker` | One choice from a short list; expands in place, or opens a full-mode picker | Toolkit `ListPicker` |
| `AccentPicker` | The full-mode grid of the 20 accents | Settings' theme page |
| `DatePicker`, `TimePicker` | A field that opens full-screen spinning columns | Toolkit `DatePicker`, `TimePicker` |
| `LoopingSelector` | One spinning column, for pickers of your own | Toolkit `LoopingSelector` |
| `TextBox` | A single- or multi-line text field with a header | `PhoneTextBox` |
| `AppBar` | Up to four round buttons, the ellipsis and a menu | Shell `ApplicationBar` |
| `MessageBox` | A caption, a message and one or two buttons over a dimmed screen | Shell `MessageBox` |
| `FullScreenPopup` | A page that swivels in over the current one; close it from inside with `usePopupClose()` | Toolkit full-mode picker page |
| `useTilt` | The tilt for anything pressable | Toolkit `TiltEffect` |
| `PhIcon`, `ph`, `GLYPHS` | Phosphor icons standing in for the phone's glyphs; `GLYPHS` names the app bar's | |
The props are in the TypeScript types. The [components page](https://wp8.vercel.app/design/components.md) has each one's metrics, states and behaviour, with live demos.
## Theme
`PhoneFrame` takes `background` (`'dark'` or `'light'`) and `accent`, an index into `ACCENTS`, the phone's 20 accents in its own order. The defaults are the emulator's: dark, and red (`DEFAULT_ACCENT`, 11).
It sets the theme's colours on its root as CSS variables, which your styles should use rather than hex values:
- `--wp-foreground`, `--wp-background`, `--wp-subtle`, `--wp-disabled`, `--wp-chrome`, `--wp-border`, and the rest of the phone's `Phone*Brush` resources.
- `--wp-accent`, with `--wp-button-base-pressed-foreground` for text on it.
`tokens.css` adds the type and spacing tokens on `:root`: the font families `--font-light`, `--font-semilight`, `--font-normal` and `--font-semibold`, the type ramp from `--size-small` (18.667 px) to `--size-extra-extra-large` (72 px), and `--wp-margin`, the 12 px unit the layout is built on. For a shell of your own, `themeVariables(background, accent)` returns the colour variables as an object for a `style` attribute.
## Keeping it Metro
The [design docs](https://wp8.vercel.app/design.md) are the full spec. The short version:
- **Lower case** for titles, page and pivot headers, list items, buttons and app bar labels; sentence case for message box captions and messages. Use the phone's own words where it has them. See [Writing](https://wp8.vercel.app/design/writing.md).
- **Colour from the variables.** One accent, used sparingly; everything else is foreground, subtle or chrome.
- **The grid.** Space in multiples of `--wp-margin`, and use the type ramp's sizes and the four weights.
- **Anything pressable is a `button` with `useTilt`.** `useTilt(onTap)` fires on release; handle Enter and Space with `onClick={(e) => { if (e.detail === 0) onTap() }}`.
- **Back closes things.** Don't draw back or close buttons on pages; register a back handler for anything Back should close, and put cancel in the app bar of forms and pickers.
- **Motion from the package.** Use the easings and transitions it exports (`EXPO_OUT_6`, `turnstile`, `swivel` and the rest), and skip animations when `prefersReducedMotion()` is true. The components already do.
## Styles
`metroweb/styles.css` is four files in this order, and each can be imported on its own:
| Import | What it holds |
|---|---|
| `metroweb/fonts.css` | `@font-face` rules for the four weights of Metroweb Sans (Segoe WP or Segoe UI when installed) |
| `metroweb/tokens.css` | The type and spacing variables on `:root` |
| `metroweb/base.css` | Page-wide rules: border-box sizing, a full-height page that doesn't scroll, unstyled buttons and the dashed accent focus ring |
| `metroweb/components.css` | The components' own classes |
To put an app inside a page that has other content, import the fonts, tokens and components without `base.css`, and give the `PhoneFrame`'s container a height.
`PhoneFrame` renders an empty frame on the server and draws the screen once it has measured its container.
## Fonts and icons
Text is set in Metroweb Sans: [Selawik](https://github.com/microsoft/Selawik), Microsoft's open-source metric match for Segoe UI, modified and renamed as its licence requires. Icons are a set of [Phosphor](https://phosphoricons.com) glyphs; an app bar button also takes an SVG fragment on its 48-unit grid. None of the phone's own fonts, icons, sounds or images are in the package.
## Licences
metroweb is under the [MIT License](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/LICENSE). Metroweb Sans is under the SIL Open Font License, Phosphor under the MIT License, and `useTilt` is a port of the Windows Phone Toolkit's `TiltEffect` (Microsoft Public License). [`THIRD-PARTY-NOTICES.md`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/THIRD-PARTY-NOTICES.md) has the details.
metroweb is an independent fan project. It is not affiliated with, endorsed by or sponsored by Microsoft. Windows and Windows Phone are trademarks of the Microsoft group of companies.
---
# Metro design system
How this recreation of Windows Phone 8 looks, moves and behaves, and where each rule lives in the code. It is written first for people building on the recreation or with the `metroweb` package, but every page should read fine without the code open.
> **Status** Draft · **Baseline** Windows Phone 8.0, with 8.1 where noted · **Last reviewed** 2026-10-19
> **Code** [`packages/metroweb/`](https://github.com/ImperialBebek/metroweb/tree/main/packages/metroweb) (the package) and [`apps/web/`](https://github.com/ImperialBebek/metroweb/tree/main/apps/web) (the phone and this site) · **Evidence** [`docs/evidence/`](https://github.com/ImperialBebek/metroweb/blob/main/docs/evidence/README.md) (tools and measurements; the Microsoft files are kept private)
## Sections
1. [Principles](https://wp8.vercel.app/design/principles.md): where Metro comes from, what it is for, how to check a screen against it, and the rules this project adds.
2. [Canvas](https://wp8.vercel.app/design/canvas.md): the 480 px logical screen, its regions, scaling and safe areas.
3. [Layout](https://wp8.vercel.app/design/layout.md): the 12 px grid, margins and component metrics.
4. [Typography](https://wp8.vercel.app/design/typography.md): Segoe WP, the type ramp and text styles.
5. [Colour and theme](https://wp8.vercel.app/design/colour-and-theme.md): light and dark palettes and the 20 accents.
6. [Iconography](https://wp8.vercel.app/design/iconography.md): glyph sizes, rings and the Phosphor substitute.
7. [Motion](https://wp8.vercel.app/design/motion.md): turnstile, feather, continuum, hub intro, tilt, live tile flips.
8. [Navigation model](https://wp8.vercel.app/design/navigation.md): Start, apps, page stacks, Back and Start.
9. [Components](https://wp8.vercel.app/design/components.md): pivot, panorama, lists, toggles, pickers, app bar, message box.
10. [Start and live tiles](https://wp8.vercel.app/design/start-and-live-tiles.md): the tile grid, templates and live behaviour.
11. [System UI](https://wp8.vercel.app/design/system-ui.md): system tray, navigation bar, volume band, desk controls.
12. [Writing](https://wp8.vercel.app/design/writing.md): casing, voice, dates and times.
13. [Web and accessibility deviations](https://wp8.vercel.app/design/web-and-accessibility.md): where the recreation departs from the phone, and why.
14. [Evidence appendix](https://wp8.vercel.app/design/evidence.md): sources, open questions and what still needs a device.
## How to read these pages
Each page starts with a header block like the one above: its status, which version of the phone it follows, the code it describes, and when it was last checked.
Values are given with their Microsoft name when one exists (`PhoneFontSizeLarge`, `PivotItemMargin`), the value, the CSS variable or file that holds it in the recreation, and a source label.
### Source labels
Every value carries one of five labels, so you can tell how much to trust it.
| Label | Meaning |
|---|---|
| **Decoded** | Read from the Windows Phone 8 emulator image: theme XAML, the registry, string tables, app manifests, or compiled UIX markup decoded with the tools in [`docs/evidence/uix/tools`](https://github.com/ImperialBebek/metroweb/tree/main/docs/evidence/uix/tools). |
| **Toolkit** | Taken from Microsoft's published Windows Phone Toolkit source (Silverlight). |
| **Fitted** | Measured frame by frame from recordings of real phones, usually with a curve fitted to the samples. |
| **Estimated** | Matched by eye against footage or screenshots, or chosen by us where no source exists. |
| **Verified** | Checked on a real device. Nothing has this label yet; see [what still needs a device](https://wp8.vercel.app/design/evidence.md#what-still-needs-a-device). |
### Settled and provisional
Where Windows Phone 8.0 and 8.1 differ, or where only 8.1 footage was available, the [version baseline](#version-baseline) below marks each choice **Settled** (we know what the phone does and chose deliberately) or **Provisional** (the best we can do without a device).
## Where the code lives
Pages name files by their path from the root of the repository.
- [`packages/metroweb/`](https://github.com/ImperialBebek/metroweb/tree/main/packages/metroweb) is the `metroweb` package, which any app can use: the tokens and styles (`styles/`), the theme, motion and page stack (`src/`), the shared components (`src/components/`), the icons (`src/icons/`) and the fonts.
- [`apps/web/`](https://github.com/ImperialBebek/metroweb/tree/main/apps/web) is the phone built on it: Start, the system tray and volume band, Settings and the other apps, and this site (`src/design/`).
- [`templates/starter/`](https://github.com/ImperialBebek/metroweb/tree/main/templates/starter) is a small app on the package, to copy when you start your own.
- [`skills/metro-ui/`](https://github.com/ImperialBebek/metroweb/blob/main/skills/metro-ui/SKILL.md) is a skill for coding agents that build on the package: the steps, the rules, and a checker for the ones code can show. [`DESIGN.md`](https://github.com/ImperialBebek/metroweb/blob/main/DESIGN.md) gives the tokens and rules in Google's DESIGN.md format.
## Ground rules
- **The phone wins.** When the phone's behaviour and a web convention disagree, the recreation follows the phone. Every exception is listed in [Web and accessibility deviations](https://wp8.vercel.app/design/web-and-accessibility.md).
- **Microsoft names are canonical.** Tokens keep the names from `ThemeResources.xaml` and the UIX markup. The CSS variables (`--wp-*`, `--size-*`, `--font-*`) are the current implementation. Shared values use those variables; a value only one app uses stays in that app's stylesheet and is listed on the page for its area.
- **No Microsoft assets ship.** Fonts, icons and images from the phone stay out of the build. See [Typography](https://wp8.vercel.app/design/typography.md) and [Iconography](https://wp8.vercel.app/design/iconography.md) for the substitutes.
## Version baseline
| Area | Windows Phone 8.0 | Windows Phone 8.1 | Recreation follows | Status |
|---|---|---|---|---|
| Back, Start, Search | Hardware or capacitive keys below the screen | Adds an optional on-screen navigation bar | 8.1 on-screen bar, because a browser has no hardware keys | Settled |
| System tray | 32 px tray across the top | Same tray, plus Action Center on a swipe down | 8.0 tray. Tapping it opens the volume band; there is no Action Center | Settled |
| Start | Four small-tile columns of solid accent tiles | Adds a six-column option and a Start background picture | 8.0 | Settled |
| App list | Search button and a list sorted by name | Search box and more sorting and grouping options | 8.0 style list; letter groups are not implemented | Settled |
| Accent colours | 20 accents from the registry | Same set (not checked against an 8.1 image) | 8.0 registry values | Settled |
| Typography | Segoe WP | Segoe WP | 8.0 theme resources | Settled |
| Page transitions | Turnstile, feather and continuum | Same family | Native turnstile and Start feather decoded from `ANIMATIONS.UIX` and `STYLES.UIX` and checked against 240 fps 8.1 footage; Toolkit timings for Silverlight-style stacks | Settled |
| Hub intro | Panorama swings in while layers slide | Same family | Fitted to 8.0 (Games hub) and 8.1 (Office hub) footage | Provisional |
| Pivot | Swipe or tap headers | Same | 8.0 behaviour; content cascade estimated from 8.1 footage | Provisional |
| Live tiles | Flip, cycle, people and calendar templates | Adds transparent tiles over a background | 8.0, decoded from `ShellControls.dll` | Settled |
| Music | One Music+Videos hub | Separate Xbox Music and Video apps | 8.0 | Settled |
| Volume | One volume band, 0 to 30, with media controls | Ringer and media volumes split | 8.0 | Settled |
| Settings | 8.0 item list and order | Adds and regroups items (for example notifications+actions) | 8.0 registry order | Settled |
## Keeping these pages current
- When you change a documented value, update its row here in the same change, including the source label.
- Screenshots, measurement diagrams and motion strips are generated. Run `npm run docs:assets` (or one group: `npm run docs:assets -- strips`) after a visual change. See [`apps/web/scripts/docs-assets.mjs`](https://github.com/ImperialBebek/metroweb/blob/main/apps/web/scripts/docs-assets.mjs).
- These pages are also published as a site at [`/design`](https://wp8.vercel.app/design.md), built from this folder by [`apps/web/scripts/design-docs.ts`](https://github.com/ImperialBebek/metroweb/blob/main/apps/web/scripts/design-docs.ts) during `npm run dev` and `npm run build`. Links between pages and to their headings become site links, and the build warns about any that don't resolve. Links outside `docs/design` (to the code or the evidence) are published as plain text, and only images in `assets/` are published.
- Each page is also published as Markdown at its route plus `.md` (`/design/motion.md`), listed in [`/llms.txt`](https://wp8.vercel.app/llms.txt) and joined with the package README in `/llms-full.txt`. Their links are absolute: pages to their `.md`, the code to GitHub.
- `DESIGN.md` and the metro-ui skill restate tokens and rules from these pages. Change them with the page.
- A `` comment on its own line puts a live component at that spot on the site, in a phone frame that follows the site's theme and accent switches. The demos are in [`apps/web/src/design/demos.tsx`](https://github.com/ImperialBebek/metroweb/blob/main/apps/web/src/design/demos.tsx); a name with no demo renders nothing and logs a warning in the dev server.
---
# 01 · Principles
> **Status** Draft · **Baseline** 8.0 · **Last reviewed** 2026-10-19
> **Code** the whole of [`packages/metroweb/`](https://github.com/ImperialBebek/metroweb/tree/main/packages/metroweb) and [`apps/web/`](https://github.com/ImperialBebek/metroweb/tree/main/apps/web) · **Sources** Microsoft's Metro design language as shipped on the phone
Metro is a small set of strong ideas. These are the ones the recreation depends on, each with what it means in this codebase.
## 1. Content is the interface
There are no borders, shadows, gradients or rounded corners around content. Text, pictures and solid colour are the interface. Chrome only appears where the system needs it: the system tray, the app bar, message boxes and the volume band, all painted in `PhoneChromeColor`.
*In code:* new components should not add decoration. If something needs to stand apart, use space (the [12 px grid](https://wp8.vercel.app/design/layout.md)) or type size, not a box.
## 2. Type does the hierarchy
Big, light Segoe WP headings, lowercase labels and generous left alignment carry the structure of every screen. A pivot header is 72 px SemiLight; a list item title is 42.667 px SemiLight; body text is 20 px.
*In code:* use the [type ramp](https://wp8.vercel.app/design/typography.md) sizes and the four family variables. Do not introduce new sizes without a source.
## 3. Motion explains where you are
Every page change turns on a hinge at the left edge of the screen. Going forward, content swings away and the new page swings in; going back reverses it. Rows leave one after another from the top, and the item you tapped leaves last, so your eye follows it into the next page.
*In code:* pages use the [feathered turnstile](https://wp8.vercel.app/design/motion.md#turnstile-feather) through `PageStack`. Mark rows with `data-feather` and the tapped label with `data-continuum` (see [Navigation model](https://wp8.vercel.app/design/navigation.md#authoring-contract)).
## 4. Personal and flat
Tiles are flat squares in the user's accent colour. Changing the accent recolours Start, toggles, selections and pressed states at once. White text sits on accent colour regardless of the theme.
*In code:* use `var(--wp-accent)` for anything that should follow the accent. Never hard-code an accent hex.
## 5. Alive in motion
Live tiles flip and cycle on their own, at random intervals, so Start never looks frozen but never moves in lockstep either.
*In code:* live behaviour only runs while Start is visible, never on small tiles, and never under reduced motion. See [Start and live tiles](https://wp8.vercel.app/design/start-and-live-tiles.md).
## 6. Touch first
Everything responds to the finger. Pressed items tilt toward the touch point and sink in. Lists scroll vertically; pivots, panoramas and Start pan horizontally. Taps that drift more than a few pixels become pans instead.
*In code:* interactive items use `useTilt`. Gestures lock to one axis after 14 px.
## Rules this project adds
These are not Metro rules; they keep the recreation honest.
1. **Source everything.** A value comes from Microsoft material when we have it, and says where it came from in a code comment and in these docs, with a [source label](https://wp8.vercel.app/design.md#source-labels).
2. **The phone wins.** When phone behaviour and web convention disagree, follow the phone and add the exception to [the deviations list](https://wp8.vercel.app/design/web-and-accessibility.md).
3. **One logical screen.** Everything is laid out at the phone's 480 px logical width and scaled as a whole. See [Canvas](https://wp8.vercel.app/design/canvas.md).
4. **No Microsoft assets in the build.** Fonts, icons and images are substituted with openly licensed equivalents.
---
# 02 · Canvas
> **Status** Draft · **Baseline** 8.0 screen, 8.1 navigation bar · **Last reviewed** 2026-10-19
> **Code** [`apps/web/src/App.tsx`](https://github.com/ImperialBebek/metroweb/blob/main/apps/web/src/App.tsx) (`useViewport`, `safeInsets`), [`apps/web/src/index.css`](https://github.com/ImperialBebek/metroweb/blob/main/apps/web/src/index.css) (`.desk`), [`packages/metroweb/styles/components.css`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/styles/components.css) (`.screen`, `.page-stack`, `.popup-layer`), [`packages/metroweb/src/components/PhoneFrame.tsx`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/src/components/PhoneFrame.tsx)
> **Sources** Windows Phone 8 logical resolution; emulator screenshots
## The logical screen
Windows Phone 8 lays every app out at a logical width of **480 px** and scales it to the panel (WVGA 480 × 800, WXGA 768 × 1280, 720p and 1080p). The recreation does the same: the whole screen is a 480 px wide element, scaled down with `transform: scale()` on viewports narrower than 480 px.
- **Width** is always 480 logical px.
- **Height** follows the viewport: the visible height, less any safe-area insets, divided by the scale.
- On wider viewports the screen sits in a centred column on the "desk", which is painted in the theme background.
An app built on the package gets the same screen from `PhoneFrame`, scaled to fit its container instead of the viewport. It has no system tray and leaves safe areas to the page around it.

*Measured from the running app at a 480 × 864 viewport, which gives a WVGA-sized 768 px page area.*
## Regions
| Region | Size | Variable | Source |
|---|---|---|---|
| System tray | 480 × 32 | `--tray-height` (0 in a home-screen web app) | Estimated, matches emulator screenshots |
| Page area | 480 × the rest | `.page-stack`, `.start`, `.popup-layer` all use `inset: var(--tray-height) 0 var(--nav-height)` | — |
| Navigation bar | 480 × 64 | `--nav-height` | Estimated, from WP8.1 footage |
The page area is the only space apps draw in. The tray and navigation bar belong to the system ([System UI](https://wp8.vercel.app/design/system-ui.md)).
## Stacking order
| Layer | `z-index` | Notes |
|---|---|---|
| Start and app pages | auto | Start is hidden (not unmounted) while an app runs. |
| Popup layer | 2 | Full-screen pickers and message boxes portal here, over the current page. |
| System tray | 3 | |
| Navigation bar | 4 | |
| Volume band | 6 | Drops over everything, including the tray. |
| Desk controls | 10 | Motion speed dial and volume keys, outside the screen. |
## Scaling, viewport height and safe areas
- `useViewport()` recomputes on `resize` and `visualViewport` resize.
- The visible height is `max(innerHeight, documentElement.clientHeight)`. iOS home-screen apps can report a short `innerHeight`, so the larger of the two wins.
- `.desk` is `position: fixed` at the top with an explicit height, so the browser can't scroll the page under the screen.
- Safe-area insets (`env(safe-area-inset-top/bottom)`) are measured once per resize with a hidden probe element. The screen is pushed down by the top inset and shortened by both. Black strips fill the insets, extending the navigation bar at the bottom.
- `index.html` asks iOS for an opaque `black` status bar. With `black-translucent`, iOS made the page a status bar shorter than the display and left a band the page couldn't draw into.
### Home-screen web apps
When launched from the home screen (`display-mode: standalone` or `navigator.standalone`), the device's own status bar is always visible, so the phone's system tray is left out and `--tray-height` becomes `0px`. Tapping the top safe-area strip stands in for tapping the tray.
## Rules
- Lay out in logical px at 480 wide. Never use viewport units inside the screen.
- Apps get the page area only. Do not position anything under the tray or navigation bar.
- Use `overflow: clip`, not `hidden`, on the screen and anything that holds perspective transforms. `hidden` lets focus or `scrollIntoView` scroll the screen itself.
---
# 03 · Layout
> **Status** Draft · **Baseline** 8.0 · **Last reviewed** 2026-10-19
> **Code** [`packages/metroweb/styles/tokens.css`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/styles/tokens.css), [`packages/metroweb/styles/components.css`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/styles/components.css), [`packages/metroweb/src/components/AppBar.tsx`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/src/components/AppBar.tsx)
> **Sources** `0100_ThemeResources.xaml` and `System.Windows.xaml` (copied to `docs/evidence/private/theme`, kept private); emulator screenshots
## The 12 px grid
Windows Phone spaces everything in multiples of 12 logical px. Text starts **24 px** from the screen's left edge almost everywhere: a 12 px page margin plus a 12 px item margin.
| Microsoft name | Value | In the recreation | Source |
|---|---|---|---|
| `PhoneMargin` | 12 | `--wp-margin`: the side margin of text blocks (paragraphs, headers above controls, toggle rows, message box text, panorama sections) | Decoded |
| `PhoneHorizontalMargin` | 12, 0 | `--wp-margin` on each side (`.paragraph`, `.toggle-row`) | Decoded |
| `PhoneVerticalMargin` | 0, 12 | `--wp-margin` above and below; not used yet | Decoded |
| `PhoneTouchTargetOverhang` | 12 | `--wp-touch-target-overhang`: the margin around touch targets (list items, buttons, text boxes, list picker and date boxes, message box buttons) | Decoded |
| `PhoneTouchTargetLargeOverhang` | 12, 20 | not used yet | Decoded |
| `PivotItemMargin` | 12, 28, 12, 0 | `.pivot-item { inset: 28px 12px 0 }` | Decoded |
| `PhoneBorderThickness` | 3 | `--wp-border-thickness`: every 3 px border (buttons, message boxes, text boxes, check boxes, the looping selector) | Decoded |
| `PhoneStrokeThickness` | 3 | `--wp-stroke-thickness`: the ring on app bar buttons, the Start arrow, app list search and the Messaging contact button, and the Camera buttons and shutter | Decoded |
| `PhoneTextBoxInnerMargin` | 1, 2 | Part of the `.textbox` padding, 4 px 3 px: the template's `Padding` 2 inside this margin | Decoded |
The variables live in `:root` in `packages/metroweb/styles/tokens.css`. Use one when a value means that resource. A 12 px that is its own measurement (the gap between tiles, the tile title inset) stays a literal.
## Pages

| Element | Value | Source |
|---|---|---|
| Pivot title | 17 px from the top of the page area, 24 px from the left | Estimated |
| Pivot headers | 21 px left padding, so the 72 px glyphs' side bearing lines up near 24 px | Estimated |
| Pivot item | 28 px below the headers, 12 px side margins | Decoded (`PivotItemMargin`) |
| List item | 12 px side padding, 12 px between items | Decoded (`PhoneTouchTargetOverhang`) |
| Paragraph | 12 px sides, 24 px below | Estimated |
| Toggle row | 72 px tall | Estimated |
| List picker | 24 px below, 44 px rows | Estimated |
| End of a scrolling page | 36 px bottom padding | Estimated |
## Start

| Element | Value | Source |
|---|---|---|
| Pane | 480 wide, padding 22 top, 24 sides, 48 bottom | Estimated |
| Grid | 4 columns of 99 px cells, 12 px gaps | Estimated, matches Microsoft's published WXGA tile sizes (159, 336, 691 px) scaled by 0.625 |
| Small tile | 99 × 99 (1 × 1 cell) | as above |
| Medium tile | 210 × 210 (2 × 2) | as above |
| Wide tile | 432 × 210 (4 × 2) | as above |
| Tile title | 12 px from the left, 6 px from the bottom | Estimated |
| App list icon | 62 × 62 accent square, 12 px gap to the title | Estimated; `StartAssets480x800` holds a 62, 62 constant (Decoded, unattributed) |
See [Start and live tiles](https://wp8.vercel.app/design/start-and-live-tiles.md) for the layout itself.
## Panorama

| Element | Value | Source |
|---|---|---|
| Section | 432 px wide, so the next section's header peeks in 48 px | Estimated |
| Section top | 146 px below the top of the page area | Estimated |
| Title | Light 133.333 px, 14 px from the left, 22 px above the top | Estimated |
| Section header | 12 px in, 18 px above the content | Estimated |
| Parallax | title moves 0.5 × its width across all sections; background moves 160 px | Estimated |
## App bar

| Element | Value | Source |
|---|---|---|
| Height | 72 collapsed, 30 minimized; grows upward when expanded | Estimated |
| Buttons | up to four, each an 80 px slot with a 48 px ring, centred, 12 px top and bottom padding, 64 px side padding | Estimated |
| Ellipsis | 64 × 36 target in the top-right corner, three 4 px dots 5 px apart | Estimated |
| Menu items | 9 px × 24 px padding, MediumLarge text | Estimated |
## Gestures
| Value | Where | Source |
|---|---|---|
| 12 px | Tap slop: a press that moves further becomes a pan and the tilt releases | Estimated |
| 14 px | Axis lock for pivot, panorama and Start pans | Estimated |
| 25% / 20% of the width | Pivot / panorama commit distance | Estimated |
| 120 px | Start pane commit distance | Estimated |
| 0.45 px/ms | Flick velocity that commits a pan regardless of distance | Estimated |
---
# 04 · Typography
> **Status** Draft · **Baseline** 8.0 · **Last reviewed** 2026-10-19
> **Code** [`packages/metroweb/styles/fonts.css`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/styles/fonts.css) (`@font-face`), [`packages/metroweb/styles/tokens.css`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/styles/tokens.css) (`--font-*`, `--size-*`), [`packages/metroweb/fonts/`](https://github.com/ImperialBebek/metroweb/tree/main/packages/metroweb/fonts)
> **Sources** `ThemeResources.xaml` (copied to `docs/evidence/private/theme`, kept private); Selawik (SIL Open Font License)
Windows Phone 8 sets everything in **Segoe WP**, in four weights, on a ramp of eight sizes. Hierarchy comes from size and weight, never from colour blocks or rules.
## Families
Each weight is its own family, as in `ThemeResources.xaml`, so components pick a weight by family name rather than `font-weight`.
| Microsoft name | Phone font | CSS variable | Recreation family | Source |
|---|---|---|---|---|
| `PhoneFontFamilyLight` | Segoe WP Light | `--font-light` | `WP Light` | Decoded |
| `PhoneFontFamilySemiLight` | Segoe WP SemiLight | `--font-semilight` | `WP SemiLight` | Decoded |
| `PhoneFontFamilyNormal` | Segoe WP | `--font-normal` | `WP Normal` | Decoded |
| `PhoneFontFamilySemiBold` | Segoe WP Semibold | `--font-semibold` | `WP SemiBold` | Decoded |
| `PhoneFontFamilyBold` | Segoe WP Bold | not defined | — | Decoded, unused |
Each `WP *` family loads, in order:
1. `Segoe WP *`, if the visitor has it installed.
2. `Segoe UI *`, which Windows has.
3. **Selawik**, from `packages/metroweb/fonts/`. Selawik is Microsoft's open-source (OFL) font made to match Segoe UI's metrics, so line lengths and wrapping come out close even where the letterforms differ.
Selawik's composite glyphs (accented letters, the ellipsis) were flattened to plain outlines with fontTools, because Chrome drew only the first component of each from the original woff2 files. Because of that change, the OFL doesn't let the files keep the name Selawik, so their file names and name tables call them Metroweb Sans; the CSS families are unaffected. If you replace the font files, flatten and rename them again.
Segoe WP itself is never shipped. The screenshots in these docs were generated on macOS, so they show Selawik.
## Type ramp
All sizes are logical px at the 480 px [canvas](https://wp8.vercel.app/design/canvas.md). Microsoft specified them in points; px = pt × 4 ÷ 3.
| Microsoft name | px | pt | CSS variable | Typical use | Source |
|---|---|---|---|---|---|
| `PhoneFontSizeSmall` | 18.667 | 14 | `--size-small` | Tile titles | Decoded |
| `PhoneFontSizeNormal` | 20 | 15 | `--size-normal` | Body text, subtitles, headers above controls | Decoded |
| `PhoneFontSizeMedium` | 22.667 | 17 | `--size-medium` | Pivot title, buttons, volume band labels | Decoded |
| `PhoneFontSizeMediumLarge` | 25.333 | 19 | `--size-medium-large` | App list titles, picker rows, text boxes, app bar menu | Decoded |
| `PhoneFontSizeLarge` | 32 | 24 | `--size-large` | Toggle state, message box captions | Decoded |
| `PhoneFontSizeExtraLarge` | 42.667 | 32 | `--size-extra-large` | List item titles | Decoded |
| `PhoneFontSizeExtraExtraLarge` | 72 | 54 | `--size-extra-extra-large` | Pivot headers, page titles | Decoded |
| `PhoneFontSizeHuge` | 186.667 | 140 | not defined | — | Decoded, unused |
Other sizes in the theme resources:
| Microsoft name | px | In the recreation | Source |
|---|---|---|---|
| `PivotHeaderFontSize` | 72 | `.pivot-header` uses `--size-extra-extra-large` | Decoded |
| `PivotTitleFontSize` | 22.667 | `.pivot-title` uses `--size-medium` | Decoded |
| `PanoramaItemHeaderFontSize` | 66 | **not followed**: `.pano-header` is 53.333 px, matched to footage of native hubs | Decoded; see [open questions](https://wp8.vercel.app/design/evidence.md#open-questions) |
| `PanoramaItemCharacterSpacing` | −35 (thousandths of an em) | not applied | Decoded |
| `LongListSelectorGroupHeaderFontSize` | 29.333 | not used | Decoded |
| `LongListSelectorGroupHeaderLetterTileFontSize` | 42.667 | not used: People's letter tiles are 38 px (Estimated) | Decoded |
| `JumpListStringFontSize` | 26.667 | not used | Decoded |
| `JumpListAlphabetFontSize` / `…SmallFontSize` | 48 / 37.333 | not used: People's jump list letters are 54 px (Estimated) | Decoded |
| `EmptyListHeaderFontSize` | 32 | not used | Decoded |
## Text styles
`ThemeResources.xaml` defines these styles. All start from `PhoneTextBlockBase`: Normal family, Normal size, the foreground colour, and a 12 px horizontal margin.
| Microsoft style | Family | Size | Colour | Recreation |
|---|---|---|---|---|
| `PhoneTextNormalStyle` | Normal | 20 | foreground | `.paragraph`, `.screen` default |
| `PhoneTextSubtleStyle` | Normal | 20 | subtle | `.paragraph-subtle`, `.list-item-subtitle`, `.toggle-header`, `.picker-header` |
| `PhoneTextTitle1Style` | SemiLight | 72 | foreground | `.pivot-header` |
| `PhoneTextTitle2Style` | SemiLight | 32 | foreground | — |
| `PhoneTextTitle3Style` | SemiLight | 22.667 | foreground | — |
| `PhoneTextExtraLargeStyle` | SemiLight | 42.667 | foreground | `.list-item-title` |
| `PhoneTextLargeStyle` | SemiLight | 32 | foreground | `.volume-value` |
| `PhoneTextGroupHeaderStyle` | SemiLight | 32 | subtle | — |
| `PhoneTextSmallStyle` | Normal | 18.667 | subtle | — (tile titles use Small in white) |
| `PhoneTextContrastStyle` | SemiBold | 20 | contrast foreground | — |
| `PhoneTextAccentStyle` | SemiBold | 20 | accent | — (`.accent-text` is Normal weight) |
| `PhoneTextHugeStyle` | Light | 186.667 | foreground | — |
| `PivotTitleStyle` | SemiBold | 22.667 | foreground | `.pivot-title`, uppercased |
All style values are **Decoded**.
## Text in components
Sizes used by shared components that are not one of the styles above. Values off the ramp have their own `--size-*` variables in `tokens.css`.
| Where | Family | Size | CSS variable | Source |
|---|---|---|---|---|
| Pivot title | SemiBold, uppercase | 22.667 | `--size-medium` | Decoded size; uppercase Estimated from screenshots |
| Toggle state (On/Off) | Light | 32 | `--size-large` | Estimated |
| Button face | SemiBold | 22.667 | `--size-medium` | Decoded (`PhoneButtonBase`) |
| Text box | Normal | 25.333 | `--size-medium-large` | Decoded (`PhoneTextBox`) |
| Tile title | Normal | 18.667 | `--size-small` | Estimated |
| Tile count | Normal | 53.333 (26.667 on small tiles) | `--size-tile-count`, `--size-tile-count-small` | Estimated |
| Calendar tile day number | Normal | 32 (42.667 on small tiles) | `--size-large`, `--size-extra-large` | Estimated |
| People tile initials | SemiLight | 26.667 | `--size-tile-initials` | Estimated |
| App list title | Normal | 25.333 | `--size-medium-large` | Estimated |
| Panorama title | Light | 133.333 | `--size-panorama-title` | Estimated |
| Panorama section header | SemiLight | 53.333 | `--size-panorama-header` | Estimated |
| App bar button label | Normal, lowercase | 16 | `--size-app-bar-label` | Estimated |
| App bar menu item | Normal, lowercase | 25.333 | `--size-medium-large` | Estimated |
| Date and time picker item | SemiLight | 54 (secondary line Normal 20) | `--size-picker-item` (`--size-normal`) | Estimated |
| Picker page title (CHOOSE TIME) | SemiBold | 20 | `--size-normal` | Estimated |
| Message box caption | SemiBold | 32 | `--size-large` | Estimated |
## Line height
| Where | Value | Source |
|---|---|---|
| Screen default | 1.33 | Estimated |
| Pivot headers, list item titles, panorama text | 1.2 | Estimated |
| Tile back text | 1.3 | Estimated |
## Rules
- Use the four `--font-*` families and the `--size-*` variables. Don't set `font-weight`; pick the family.
- Don't add a size without a source. If a component needs one, add it to the table above with its label.
- Headings and labels are left-aligned. Nothing is centred except tile glyphs and picker items.
- Letter case is part of the type system: see [Writing](https://wp8.vercel.app/design/writing.md).
- The desk controls beside the phone (13–15 px) are outside the phone and not part of the ramp.
## App-specific sizes
Sizes that only one app uses stay in that app's stylesheet. All are **Estimated**. If a second component needs one, move it to a `--size-*` variable in `tokens.css`.
| App | Where | Size |
|---|---|---|
| Alarms | Alarm time in the list | 64 |
| Alarms | Time on the ringer | 128 |
| Phone | Keypad digits | 40 (30 on the in-call keypad, its letters 14) |
| Phone | Caller name during a call | 54 |
| People | Initials on favourites | 36 |
| People | Letter tiles in the contact list | 38 |
| People | Initials on group tiles | 18 |
| People | Jump list letters | 54 |
| Calculator | Digit keys | 36 (operators and functions 30) |
| Calendar | Appointments in month cells | 12 |
| Messaging | Time under a bubble | 16 |
| Messaging | Character counter | 15 |
| Camera | Labels on the bar | 14 |
---
# 05 · Colour and theme
> **Status** Draft · **Baseline** 8.0 · **Last reviewed** 2026-10-19
> **Code** [`packages/metroweb/src/theme.ts`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/src/theme.ts) (`ACCENTS`, `PALETTES`, `themeVariables`), [`apps/web/src/store.tsx`](https://github.com/ImperialBebek/metroweb/blob/main/apps/web/src/store.tsx), [`apps/web/src/App.tsx`](https://github.com/ImperialBebek/metroweb/blob/main/apps/web/src/App.tsx)
> **Sources** `0000_ThemeResources.xaml` (light) and `0100_ThemeResources.xaml` (dark); the image's registry (accents, defaults); `uixmobileres.dll.mui` (accent names)
The user picks two things: a **background** (dark or light) and an **accent** (one of 20 colours). Everything else follows from those.
 
## How the theme is applied
- `themeVariables(background, accent)` turns the palette into CSS variables named after the Microsoft resources: `subtle` becomes `--wp-subtle`, `textBoxEditBackground` becomes `--wp-text-box-edit-background`.
- `App` sets the variables on `.desk` and `.screen`, and `data-theme="dark|light"` on the screen.
- The page background and the `theme-color` meta tag follow the background (`#000000` or `#ffffff`), so overscroll and browser chrome match.
- Settings are saved in `localStorage` (`wp8-settings`). The default is **dark** with **Red**, from the emulator registry (`DefaultTheme = 1`, `DefaultAccent = 0xB`).
## Palette
XAML colours are `#AARRGGBB`; the recreation converts them to `rgba()`. All values are **Decoded**.
| Microsoft name | CSS variable | Dark | Light | Used for |
|---|---|---|---|---|
| `PhoneForegroundColor` | `--wp-foreground` | `#FFFFFFFF` | `#DE000000` | Text, glyphs, borders |
| `PhoneBackgroundColor` | `--wp-background` | `#FF000000` | `#FFFFFFFF` | Page background |
| `PhoneContrastForegroundColor` | `--wp-contrast-foreground` | `#FF000000` | `#FFFFFFFF` | Text on a foreground-coloured fill |
| `PhoneContrastBackgroundColor` | `--wp-contrast-background` | `#FFFFFFFF` | `#DE000000` | |
| `PhoneSubtleColor` | `--wp-subtle` | `#99FFFFFF` | `#66000000` | Subtitles, headers above controls |
| `PhoneDisabledColor` | `--wp-disabled` | `#66FFFFFF` | `#4D000000` | Disabled text and controls |
| `PhoneChromeColor` | `--wp-chrome` | `#FF1F1F1F` | `#FFDDDDDD` | App bar, volume band, message boxes |
| `PhoneBorderColor` | `--wp-border` | `#BFFFFFFF` | `#99000000` | |
| `PhoneSemitransparentColor` | `--wp-semitransparent` | `#AA000000` | `#AAFFFFFF` | Message box scrims |
| `PhoneTextBoxColor` | `--wp-text-box` | `#BFFFFFFF` | `#26000000` | Text box fill at rest, list picker border |
| `PhoneTextBoxForegroundColor` | `--wp-text-box-foreground` | `#FF000000` | `#DE000000` | Text in text boxes |
| `PhoneTextBoxEditBackgroundColor` | `--wp-text-box-edit-background` | `#FFFFFFFF` | `#00000000` | Text box and open picker while editing |
| `PhoneTextBoxReadOnlyColor` | `--wp-text-box-read-only` | `#77000000` | `#2E000000` | Text in a read-only text box |
| `PhoneTextBoxSelectionForegroundColor` | `--wp-text-box-selection-foreground` | `#FFFFFFFF` | `#FFFFFFFF` | Selected text in a text box, on the accent |
| `PhoneTextCaretColor` | `--wp-text-caret` | `#FF000000` | `#DE000000` | Text box caret |
| `PhoneButtonBasePressedForegroundColor` | `--wp-button-base-pressed-foreground` | `#FFFFFFFF` | `#FFFFFFFF` | Text and glyphs on accent: pressed buttons, tiles, selected items, accent toasts and bubbles |
| `PhonePivotUnselectedItemOpacity` | `--wp-pivot-unselected-opacity` | 0.4 | 0.23144 | Pivot headers that aren't selected |
| `PhoneAccentColor` | `--wp-accent` | the accent | the accent | Tiles, toggles, selection, pressed states |
Resources in the XAML that the recreation doesn't use yet:
| Microsoft name | Dark | Light |
|---|---|---|
| `PhoneProgressBarBackgroundColor` | `#19FFFFFF` | `#19000000` |
| `PhoneTextHighContrastColor` | `#FFFFFFFF` | `#DE000000` |
| `PhoneTextMidContrastColor` | `#99FFFFFF` | `#73000000` |
| `PhoneTextLowContrastColor` | `#73FFFFFF` | `#40000000` |
| `PhoneInactiveColor` (deprecated) | `#33FFFFFF` | `#33000000` |
| `PhoneInverseInactiveColor` (deprecated) | `#FFCCCCCC` | `#FFE5E5E5` |
| `PhoneInverseBackgroundColor` (deprecated) | `#FFFFFFFF` | `#FFDDDDDD` |
## Accents
Registry order, which is also the order of the accent picker grid. Names from `uixmobileres.dll.mui`. All **Decoded**. The last column is the contrast of white text on the accent, which is what tiles and pressed buttons show.
| # | Name | Hex | White on accent |
|---|---|---|---|
| 0 | Lime | `#A4C400` | 2.00 |
| 1 | Green | `#60A917` | 2.92 |
| 2 | Emerald | `#008A00` | 4.53 |
| 3 | Teal | `#00ABA9` | 2.84 |
| 4 | Cyan | `#1BA1E2` | 2.90 |
| 5 | Cobalt | `#0050EF` | 6.19 |
| 6 | Indigo | `#6A00FF` | 6.87 |
| 7 | Violet | `#AA00FF` | 5.06 |
| 8 | Pink | `#F472D0` | 2.57 |
| 9 | Magenta | `#D80073` | 5.04 |
| 10 | Crimson | `#A20025` | 8.19 |
| 11 | Red (default) | `#E51400` | 4.74 |
| 12 | Orange | `#FA6800` | 2.98 |
| 13 | Amber | `#F0A30A` | 2.11 |
| 14 | Yellow | `#E3C800` | 1.68 |
| 15 | Brown | `#825A2C` | 6.09 |
| 16 | Olive | `#6D8764` | 3.96 |
| 17 | Steel | `#647687` | 4.68 |
| 18 | Mauve | `#76608A` | 5.51 |
| 19 | Taupe | `#87794E` | 4.31 |

## Contrast of the theme colours
The phone's own colours, measured against the background they sit on. They are kept as they are (the phone wins), but don't use subtle or disabled colours for anything a user must read to finish a task. See [Web and accessibility deviations](https://wp8.vercel.app/design/web-and-accessibility.md#contrast).
| Pair | Dark | Light |
|---|---|---|
| Foreground on background | 21.0 | 16.1 (87% black) |
| Subtle on background | 7.37 | 2.85 |
| Disabled on background | 3.66 | 2.12 |
| Unselected pivot header (foreground at the unselected opacity) | 3.66 | 1.61 |
## Rules
- Anything that should follow the accent uses `var(--wp-accent)`. Never write an accent hex in a component.
- Text on accent colour uses `--wp-button-base-pressed-foreground`, which is white in both themes. White that sits over a picture rather than the accent (Camera, the Photos viewer, Music+Videos over album art) and the black navigation bar are written as `#fff`.
- Chrome surfaces (app bar, volume band, message boxes) use `--wp-chrome`, never the background.
- Use the palette variables. If you need a colour that isn't in the XAML, derive it with `color-mix()` from a palette variable, label it Estimated, and add it below.
## Derived colours
Colours the recreation mixes itself. All **Estimated**.
| Where | Value | Why |
|---|---|---|
| People tile cells | accent mixed with 30% or 45% black, or 15% white | Contacts have no photos, so cells are shaded initials cards |
| Panorama background dim | background at 55% over the picture | Keeps hub text readable over photos |
| Volume slider track | `--wp-subtle` at 0.4 opacity | Matched to footage |
| Text box placeholder | text box foreground at 55% | `PhoneTextBox` has no placeholder; Messaging's compose box shows one |
| Music tile title | `text-shadow: 0 0 6px rgba(0,0,0,0.6)` | White title over album art |
| Navigation bar and safe areas | `#000` in both themes | The 8.1 navigation bar is black |
| Photos viewer info bar | black at 60% over the picture | Keeps the date and time readable over any photo |
| Photos viewer app bar | `#1F1F1F` (dark chrome) at 90% | Stays dark and see-through over pictures in both themes |
| Camera bar | black at 50% | Over the viewfinder |
| Music+Videos over album art | black at 60% (history labels), 45% (now playing toggles), 65% (seek bar); white at 35% (seek track) | Keeps text and controls readable over any cover |
Message box scrims aren't derived: [`MessageBox`](https://wp8.vercel.app/design/components.md#message-box) uses `--wp-semitransparent`.
---
# 06 · Iconography
> **Status** Draft · **Baseline** 8.0 · **Last reviewed** 2026-10-19
> **Code** [`packages/metroweb/src/icons/`](https://github.com/ImperialBebek/metroweb/tree/main/packages/metroweb/src/icons) (`phosphor.ts`, `PhIcon.tsx`, `ph.tsx`), [`packages/metroweb/src/components/glyphs.tsx`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/src/components/glyphs.tsx), [`apps/web/src/start/icons.tsx`](https://github.com/ImperialBebek/metroweb/blob/main/apps/web/src/start/icons.tsx), [`packages/metroweb/scripts/gen-phosphor-icons.mjs`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/scripts/gen-phosphor-icons.mjs)
> **Sources** Phosphor Icons (MIT, [`LICENSE-phosphor.txt`](https://github.com/ImperialBebek/metroweb/blob/main/packages/metroweb/src/icons/LICENSE-phosphor.txt)); sizes Estimated from emulator screenshots
Metro glyphs are flat, solid, single-colour shapes drawn on a square grid, with no outlines, gradients or perspective. The phone's own glyphs (Segoe WP symbols and the SDK's app bar icon set) can't ship, so the recreation uses **Phosphor** in its **fill** weight, which has the same solid, geometric character.
## Where icons come from
- `packages/metroweb/scripts/gen-phosphor-icons.mjs` copies the path data of the icons named in its `NAMES` list from `@phosphor-icons/core` into `packages/metroweb/src/icons/phosphor.ts`. Every icon is on a 256-unit square.
- Icons use the fill weight. An entry can name another weight as `icon:weight`; only two do: `star:regular` (the outline "favorite" star, so it reads differently from the filled "unfavorite" one) and `check:bold`.
- `phosphor.ts` is generated. Never edit it by hand.
Three ways to draw an icon:
| Helper | Use for | Notes |
|---|---|---|
| `` | A standalone icon (system tray, navigation bar, volume band) | An `