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.
Store CTA
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. The rise is a view timeline; where there is none, or with reduced motion, the phone stands where the rise ends. The store buttons invert on the panel, the block's text as their ground. Zero JavaScript.
Live demo
Start tomorrow morning
Free for three habits. Every habit, the widgets and the watch for $3.99 a month (a sample price).
- Walk 20 minutes7:40
- Read 10 pages
- No phone after 10
- Water the plants
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 29.7 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 view timelines the phone stands still where the rise ends.
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/store-cta---
import StoreCta from '@/components/moonarc/pro/StoreCta.astro';
---
<StoreCta
copy="Free for three habits. Every habit for $3.99 a month."
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: 'Scan to get the app' }}
screen={{ title: 'Today', label: 'The today screen', rows: [{ kind: 'ring', value: 0.75, text: '3 of 4', label: 'done' }] }}
>
<span slot="headline">Start <em>tomorrow</em> morning</span>
</StoreCta>Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: phone-frame, app-screen, reveal.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
stores | { href: string; name: string; note?: string }[] | none | The stores as text buttons. Slot "actions" replaces them and the QR code. |
screen | AppScreen props | none | The screen in the phone; a label names it for a screen reader. |
qr | { value: string; caption?: string; label?: string } | none | A QR code to the app, on a wide screen with a pointer. |
headline | string | none | The heading. Slot "headline" wins and may hold an <em>. |
eyebrow | string | none | A short line above the heading. |
copy | string | none | A line under the heading. |
note | string | none | A short line under the buttons. |
block | 1 | 2 | 3 | 4 | 5 | 1 | The panel's colour block. |
model | 'island' | 'notch' | 'punch' | 'island' | The phone's cut-out. |
level | 2 | 3 | 2 | Heading level of the heading. |
tone | 'surface' | 'muted' | 'ink' | 'brand' | unset (paints nothing) | The ground around the panel. |
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-scta.ma-scta__actions.ma-scta__copy.ma-scta__eyebrow.ma-scta__headline.ma-scta__media.ma-scta__note.ma-scta__outer.ma-scta__panel.ma-scta__phone
Slots
slot="headline"slot="actions"
Reduced motion
The phone stands still where the rise ends.
With ClientRouter
CSS and Reveal.
Why it is built this way
- The phone is cut by the panel (overflow: clip on the panel), so it reads as standing behind the panel's edge, and the rise is a translate on the panel's own view timeline: it arrives as the reader arrives, and goes back down if they scroll back.
- On a wide screen the phone stands at the panel's end and the panel is at least as tall as the phone's visible part, so a short headline never lets the phone run out of the top.
- The store buttons are the block's text on the block's ground, inverted: the pair the palette already holds at 4.5 : 1.
Replaces: App download banners with store badges and a phone mockup. See the migration table.
Related
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.
App Hero
ProA 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.
Block CTA
ProThe closing call on a colour block: a short line, a large heading that ends on a phrase in the emphasis face, a sentence and two pill actions, on one of the palette's blocks with a drawing (Motif) rising out of its lower corner.
A closing call to action: a deep brand panel with an aurora and film grain behind it, a headline that catches a shine, a magnetic primary action, a secondary one, and a typed command.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown