A phone app's screen drawn from rows instead of a screenshot: a status bar, a title, then rows of fourteen kinds (a big figure, a ring of progress, a checklist, a bar chart, a card, chips, a map with a route, a player, a chat, a list, a week of days, a button, a lock screen's clock, a notification) and a tab bar.
App Hero
A hero for a mobile app: the headline, the rating line (stars, the average, how many rated and where) and the store buttons beside a phone on a disc of the app's colour. The phone's screen is drawn from rows (AppScreen), so a palette recolours it and no screenshot goes stale; two or three screens take their turn once, then the first returns and rests, and the phone holds still while it is pointed at. On a wide screen a QR code to the app sits beside the buttons, since a reader on a laptop has the phone in their other hand. A card can float beside the phone, the way a notification would. The store buttons are text, as neither store lets its badge be redrawn. Zero JavaScript.
Live demo
For iPhone and Android
Keep the small promises
Pick up to four small habits. One tap a day keeps the streak, and the week shows what stuck.
Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.
Variants · each row writes the attribute the prop writes; the section's own CSS answers
Measured
JavaScript of its own
0 B
Uses Reveal and the shared runtime (1.9 kB raw, once per site). With those included: 3.0 kB raw.
CSS 33.9 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 105 · Firefox 110 · Safari 16; needs container-units. Elsewhere: without container units the drawn screens keep the page's text size.
Install Pro
The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:
npx shadcn@latest add @moonarc-pro/hero-app---
import HeroApp from '@/components/moonarc/pro/HeroApp.astro';
---
<HeroApp
eyebrow="For iPhone and Android"
copy="Small habits, one tap a day. The streak keeps itself."
stores={[
{ href: 'https://apps.apple.com/app/id000000000', note: 'Download on the', name: 'App Store' },
{ href: 'https://play.google.com/store/apps/details?id=com.example', note: 'Get it on', name: 'Google Play' },
]}
qr={{ value: 'https://example.com/get', caption: 'Point your phone\'s camera here' }}
rating={{ value: 4.8, count: '12,400', source: 'on the App Store' }}
phoneLabel="The app's today screen: three of four habits done"
screens={[
{ title: 'Today', rows: [{ kind: 'ring', value: 0.75, text: '3 of 4', label: 'habits done' }, { kind: 'checks', items: [{ text: 'Walk', done: true }, { text: 'Read' }] }] },
{ title: 'This week', rows: [{ kind: 'bars', values: [3, 4, 2, 4, 4, 3, 1], labels: ['M', 'T', 'W', 'T', 'F', 'S', 'S'], highlight: 4 }] },
]}
callout={{ app: 'Sitename', title: '12 days in a row', text: 'Your longest streak yet' }}
>
<span slot="headline">Keep the <em>small</em> promises</span>
</HeroApp>Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: split-text, reveal, phone-frame, app-screen.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
stores | { href: string; name: string; note?: string }[] | none | The stores as text buttons: note is the small line over the name ("Download on the" over "App Store"). Slot "actions" replaces them and the QR code, for the stores' official badges. |
screens | AppScreen props[] | none | One to three screens. The first is shown at once and is the one at rest; each other one takes a turn of 3.2 s from the third second, then the first returns. |
phoneLabel | string | none | What the phone shows, for a screen reader: one name for the picture. Unset, the phone is decoration. |
qr | { value: string; caption?: string; label?: string } | none | A QR code to the app beside the buttons, on a wide screen with a pointer only. value is what it opens; label is what a screen reader hears (default "QR code to" and the value). |
rating | { value: number; count: string; source?: string } | none | The rating line: "4.8 out of 5 from 12,400 ratings on the App Store". count is written as given. |
callout | { title: string; text?: string; app?: string } | none | A card floating over the disc's edge, beside the phone. Decoration: hidden from a screen reader. |
headline | string | none | The headline, split by word. Slot "headline" wins and may hold an <em>. |
eyebrow | string | none | A short line above the headline. Slot "eyebrow" wins. |
copy | string | none | A paragraph under the headline. |
note | string | none | A short line under the buttons. |
block | 1 | 2 | 3 | 4 | 5 | 1 | The colour block of the disc and the callout's icon. |
model | 'island' | 'notch' | 'punch' | 'island' | The phone's cut-out (PhoneFrame). |
level | 1 | 2 | 1 | Heading level of the headline. |
media | 'right' | 'left' | 'right' | Side of the phone on a wide screen. |
tone | 'surface' | 'muted' | 'ink' | 'brand' | unset (paints nothing) | The hero's ground. |
density | 'compact' | 'cozy' | 'roomy' | 'cozy' | Block padding ×0.8 / ×1 / ×1.25 through --ma-density. |
Theming and overrides
The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.
Classes
.ma-happ.ma-happ__actions.ma-happ__app.ma-happ__callout.ma-happ__copy.ma-happ__ctext.ma-happ__ctitle.ma-happ__disc.ma-happ__eyebrow.ma-happ__headline.ma-happ__icon.ma-happ__inner.ma-happ__media.ma-happ__note.ma-happ__phone.ma-happ__rating.ma-happ__screen.ma-happ__screens.ma-happ__text.ma-in.ma-stars
Slots
slot="eyebrow"slot="headline"slot="actions"
Reduced motion
The first screen only, with no turns; the callout is there at once.
With ClientRouter
CSS and Reveal; a fresh page plays the turns from the start.
Why it is built this way
- The turns are CSS delays written at build: each later screen fades in at its moment and out when the first returns, so the phone rests on the screen the page leads with, and nothing moves beside the headline after ten seconds.
- Each screen's own motion (its ring filling, its bars growing) is delayed to the screen's turn through --ma-as-at, so a screen fills as it is shown, not while it is hidden.
- Pointed at or holding focus, the phone holds still: the turns and the screens' own motion pause together.
- The QR code is for a wide screen with a pointer: on a phone it would ask the reader to scan the screen they are holding.
- The store buttons are the page's ink on its ground, the colour pair the stores' own badges use, on any palette.
Replaces: App landing page heroes with phone mockups · App download sections with store badges. See the migration table.
Related
A generic phone frame around a screenshot or any content, with three cut-outs (island, notch, punch), a home bar, and no logo, buttons or brand.
QR Code
DataA QR code for a URL or a short text, encoded at build and drawn as one SVG path inside the quiet zone a reader needs.
An app's features told down the page while one phone stays in view beside them and its screen changes with each feature.
Store CTA
ProThe call to get the app: a panel of the app's colour block with the headline, the store buttons and, on a wide screen, a QR code to the app, and a phone rising out of the panel's lower edge as the panel comes into view, its lower third cut by the panel.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown