Skip to content

Components / Sections

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.

4.8 out of 5 from 12,400 ratings on the App Store (sample figures)

Point your phone's camera here to open this page

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

tone
density
media

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

Baseline · widely available

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:

terminal
npx shadcn@latest add @moonarc-pro/hero-app
src/pages/index.astro
---
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

PropTypeDefaultDescription
stores{ href: string; name: string; note?: string }[]noneThe 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.
screensAppScreen props[]noneOne 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.
phoneLabelstringnoneWhat the phone shows, for a screen reader: one name for the picture. Unset, the phone is decoration.
qr{ value: string; caption?: string; label?: string }noneA 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 }noneThe 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 }noneA card floating over the disc's edge, beside the phone. Decoration: hidden from a screen reader.
headlinestringnoneThe headline, split by word. Slot "headline" wins and may hold an <em>.
eyebrowstringnoneA short line above the headline. Slot "eyebrow" wins.
copystringnoneA paragraph under the headline.
notestringnoneA short line under the buttons.
block1 | 2 | 3 | 4 | 51The colour block of the disc and the callout's icon.
model'island' | 'notch' | 'punch''island'The phone's cut-out (PhoneFrame).
level1 | 21Heading 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

Replaces: App landing page heroes with phone mockups · App download sections with store badges. See the migration table.

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.

every browserload

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.

every browseralways

A 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.

every browseralways

An app's features told down the page while one phone stays in view beside them and its screen changes with each feature.

Chrome 116 · not Firefox · Safari 26scroll

The 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.

every browserscroll

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown