Push notification
Push notification shows how a notification looks on iPhone and Android, a Live Activity on the iPhone Lock Screen, and the phone they appear on. Use it for previews, notification settings and documentation.
Designed in code from the owner's two references (a ride Live Activity and a countdown Live Activity) and reviewed on real device sizes. Beta until the Android 16 Live Update behaviour is confirmed against the oldest Android version we support, and the final Nuton app icon replaces the placeholder. Everything on this page is implemented and tested.
Preview
Switch the platform, appearance, expanded state and copy length. Tap the notification or an action to see what it would do.
Tap the notification or an action
import { PushNotification } from '@nuton/ui'
<PushNotification
title="Section 3 is ready"
body="Pick up Form and Function where you left off. About 8 minutes."
time="7:30 pm"
actions={['Start section', 'Tomorrow']}
onOpen={openSection}
onAction={runAction}
/>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactimport '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'Usage
import { Nuri, PushNotification } from '@nuton/ui'
<PushNotification
title="Section 3 is ready"
body="Pick up Form and Function where you left off. About 8 minutes."
time="7:30 pm"
onOpen={() => router.push('/lessons/form-and-function/3')}
/>What this is, and is not
It draws a notification the way the operating system does, so designers, reviewers and the settings screens can see exactly what people will receive. It does not send, schedule or register anything: push delivery, Live Activities and Android's ongoing notifications are built in the native apps and the notification service.
- Write for the system surface: a short title, one line, and (for a badge, a milestone or a welcome back) a small
artimage. Nothing is tinted or restyled, so people recognise it as a notification. - The app icon is the identity. Use
artsparingly: Nuri beside a reward or a welcome, never on every notification. - Set
appearanceto the phone's mode, not the app's theme. A phone in dark mode shows dark notifications whatever the app is set to. - Use
platform="android"for Android. It is a different design (app row, one-line body, actions on the notification), not the iPhone one redrawn. - Use
LiveActivityonly when the time left or the progress is itself the message: a streak countdown, a lesson in progress. A reminder, a badge or a recap stays an ordinary notification. - Put notifications inside a
PushPhoneto judge them where people see them: the iPhone Lock Screen, another app, or the Android shade, at the device's real proportions. Notifications inside follow its platform and appearance. - Scale it by setting
--nu-push-u(one point) on an ancestor. The default is one CSS pixel, which is the real device size.
Variants and sizes
Sizes are real device sizes: iOS points and Android dp, on an iPhone 16 (393 × 852 pt) and a Pixel (412 × 915 dp). The previews are scaled to fit this column.
iPhone
26 radius, 14 padding, a 38 app icon (9 radius). The title is 15 semibold, up to two lines; the line is 15/20, up to three; the time is 13. Expanded (touch and hold) is a 28 radius card with a 30 icon and 17/22 text, and the actions sit in their own 22 radius list.
<PushNotification title="12 cards are ready" body="A 3-minute review keeps what you learned fresh." time="12:10 pm" />
<PushNotification
title="New badge: First Flashcard Session"
body="You finished your first review. See it in your collection."
time="4:41 pm"
art={<Nuri size={32} crop="head" decorative animated={false} />}
/>
<PushNotification expanded title="Section 3 is ready" body="Pick up Form and Function where you left off." time="7:30 pm"
actions={['Start section', 'Tomorrow']} />Android
28 radius (6 where grouped), 16 and 18 padding, a 20 app icon in the app row, a 48 image (12 radius). The title is 16 medium on one line; the line is 14/20, one line collapsed and up to four expanded. Actions are 36 tall pills. A group is a shared header with the corners rounded as the system rounds them.
<PushNotification platform="android" title="12 cards are ready" body="A 3-minute review keeps what you learned fresh." time="12:10 pm" />
<PushNotification platform="android" expanded title="Section 3 is ready" body="Pick up Form and Function where you left off." time="7:30 pm"
actions={['Start section', 'Tomorrow']} onToggle={toggle} />
<PushNotification platform="android" group="first" … />
<PushNotification platform="android" group="middle" … />
<PushNotification platform="android" group="last" … />Live Activity
28 radius, dark in either appearance, as on iOS. The title is the one large element (50 bold; 38 for a success state like “Streak safe”; 30 with a track), never wraps and is cut with an ellipsis rather than overlap the icon; the note is one short sentence (17/22); the app icon is 76 (56 beside a track). status turns the title red for the last hour or green once the goal is met.
<LiveActivity label="12" labelIcon={<RiFireLine />} title="2:00:45" note="Your 12-day streak is still within reach." />
<LiveActivity label="12" labelIcon={<RiFireLine />} title="0:50:01" status="urgent" note="One section is enough." />
<LiveActivity label="12" title="Streak safe" status="success" note="See you tomorrow." />
<LiveActivity title="6 min left" note="Form and Function" chip="3 of 6" progress={0.5}
marker={<Nuri size={32} crop="head" decorative animated={false} />} />Phone
The device and the system surface around a notification: the iPhone Lock Screen (date, clock, flashlight and camera), another app being used (where a banner drops in), the touch-and-hold backdrop, and the Android notification shade with quick settings. It fills its container, keeps the device's real proportions and scales everything in it, so a notification inside is the size it is on the device. The status bar, wallpaper and quick settings are decoration, hidden from assistive technology; what you put inside is real.
<PushPhone label="iOS Lock Screen" time="21:59">
<LiveActivity label="12" title="2:00:45" note="Your 12-day streak is still within reach." />
<PushNotification title="12 cards are ready" body="A 3-minute review keeps what you learned fresh." time="12:10 pm" />
</PushPhone>
<PushPhone screen="app" label="iOS banner over another app">
<PushNotification title="Section 3 is ready" body="…" time="now" onOpen={open} />
</PushPhone>
<PushPhone dimmed label="iOS expanded notification">
<PushNotification expanded title="Section 3 is ready" body="…" actions={['Start section', 'Tomorrow']} />
</PushPhone>
<PushPhone platform="android" label="Android notification shade">
<PushNotification group="first" … />
<PushNotification group="last" … />
</PushPhone>Phone sizes
Three sizes per platform, in the device's own points (iPhone) or dp (Android), for checking how a notification truncates and wraps: iPhone 375 × 812, 393 × 852 (the iPhone 16) and 440 × 956; Android 360 × 800, 412 × 915 (a Pixel) and 600 × 960. The previews below are drawn at one width on the page, so the larger devices show the same notification smaller, as they would on a screen.
<PushPhone size="compact" label="iPhone compact">…</PushPhone>
<PushPhone size="standard" label="iPhone 16">…</PushPhone>
<PushPhone size="large" label="iPhone large">…</PushPhone>Colours and contrast
These are the system's surfaces, so they use the colour primitives and follow the phone's appearance, not the Nuton theme. Contrast was computed from the token values, against the lightest background each can sit on.
| Pair | Contrast |
|---|---|
| iOS light: title and line · secondary text | 17.18:1 · 4.61:1 |
| iOS dark: title and line · secondary text | 15.72:1 · 6.84:1 |
| Android light: secondary text · action | 4.67:1 · 5.79:1 |
| Android dark: secondary text · action | 7.25:1 · 6.60:1 |
| Live Activity: title · note · urgent · success | 13.77:1 · 7.25:1 · 4.91:1 · 8.02:1 |
The tests run axe in jsdom, which cannot check contrast, so the figures above are computed, not measured in a browser.
States and interactions
onOpen it is a button with a 2px primary focus ring.When the copy is long
| Surface | Title | Line |
|---|---|---|
| iOS banner and Lock Screen | Up to two lines, then an ellipsis | Up to three lines, then an ellipsis |
| iOS expanded | Full | Full |
| Android collapsed | One line, ellipsis | One line, ellipsis |
| Android expanded | One line, ellipsis | Up to four lines, then an ellipsis |
| Live Activity | Never wraps | The note wraps |
State tokens
Set on .nu-push and .nu-live: --nu-push-u (one point; scale the whole notification), --nu-push-text, --nu-push-sub, --nu-push-surface, --nu-push-solid, --nu-push-hair, --nu-push-chip and --nu-push-accent. The dark values are set by data-appearance="dark".
Decisions still open
- The app icon is a placeholder: Nuri's head on a light tile. It should be replaced by the Nuton app icon through
icon. - Android 16 has a purpose-built Live Update for an ongoing countdown. Earlier versions use an ordinary ongoing notification with a chronometer. Confirm against the oldest Android version we support before building it.
- Glass (the blur behind a notification) needs a real browser; there is a solid fallback where backdrop-filter is not supported, which has not been checked in Safari.
Composition examples
A stack that opens
The component draws one notification. A stack, Show Less and Clear are the screen's own controls, and each notification reports its tap with onOpen.
The full set of Nuton learning notifications, drawn on both platforms with the rules for when each is sent, is in the notification review at /design-exploration/notifications/.
API reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
platform | 'ios' | 'android' | 'ios' | Which system to draw. They are different designs. |
appearance | 'light' | 'dark' | 'light' | The phone's appearance, not the app theme. |
titlerequired | string | — | Short and specific. iOS: up to two lines. Android: one line. |
bodyrequired | string | — | One supporting line. iOS: up to three lines. Android: one line, or four when expanded. |
time | string | 'now' | "now", "7:30 pm", "Mon 9:00 am". |
appName | string | 'Nuton' | Shown on Android and in the expanded iOS view. |
icon | ReactNode | — | The app icon. Defaults to the Nuton icon (a placeholder). |
art | ReactNode | — | A small image on the right. Leave it out for everything but a badge, a milestone and a welcome back. |
actions | readonly string[] | — | Up to two short verbs, shown when expanded. |
expanded | boolean | false | iOS: the touch-and-hold view with actions beneath. Android: the whole line and the actions. |
onOpen | () => void | — | Makes the notification a button that opens its destination. |
onAction | (index: number, label: string) => void | — | Called when an action is pressed. |
progress | number | — | Android: 0 to 1. Shows a progress bar under the text, for an ongoing countdown. |
onDismiss | () => void | — | iOS expanded: shows the close button. Called when it is pressed. |
dismissLabel | string | 'Dismiss' | Accessible name of the iOS close button. |
onToggle | () => void | — | Android: shows the expand chevron. Ignored on iOS. |
toggleLabel | string | — | Accessible name of the chevron. Defaults to Expand or Collapse. |
group | 'first' | 'middle' | 'last' | — | Android: the position in a group, to round the shared corners. |
…section props | HTMLAttributes<HTMLElement> | — | className, id and other attributes go to the container. The ref points at it. |
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | Names the phone for screen readers, such as "iOS Lock Screen, grouped". |
platform | 'ios' | 'android' | 'ios' | iPhone or Android. |
appearance | 'light' | 'dark' | 'light' | The phone's mode. Notifications inside follow it unless they say otherwise. |
size | 'compact' | 'standard' | 'large' | 'standard' | iPhone 375 × 812, 393 × 852, 440 × 956. Android 360 × 800, 412 × 915, 600 × 960. |
screen | 'lock' | 'app' | 'lock' | iPhone only. The Lock Screen, or another app where a banner drops in. Android always shows the shade. |
dimmed | boolean | false | iPhone Lock Screen: the touch-and-hold backdrop, with what is inside placed lower. |
time | string | '9:41' | The clock. |
date | string | — | The date line. Defaults to "Thursday 22 October" (iPhone) or "Thu, 22 Oct" (Android). |
children | ReactNode | — | The notifications, Live Activities and any controls the screen needs. |
…div props | HTMLAttributes<HTMLDivElement> | — | className, id and other attributes go to the outer container. The ref points at it. |
| Prop | Type | Default | Description |
|---|---|---|---|
titlerequired | string | — | The answer, large: a countdown, a state or a time left. It never wraps. |
note | string | — | One short supporting sentence. |
label | string | — | A small line above the title, such as a count. |
labelIcon | ReactNode | — | Icon beside the label. |
icon | ReactNode | — | The app icon on the right. Defaults to the Nuton icon. |
status | 'default' | 'urgent' | 'success' | 'default' | urgent turns the title red (the last hour); success turns it green. |
progress | number | — | 0 to 1. Adds a track with a marker, for something that progresses. |
marker | ReactNode | — | Small artwork riding the track. |
chip | string | — | A short count at the end of the top row in the track layout, such as "3 of 6". |
aria-label | string | 'Live Activity' | Accessible name of the group. |
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves to the notification (with onOpen), then the actions, then the Android chevron |
| Enter / Space | Opens the notification or runs the focused action |
Screen readers
A tappable notification is one button named by its title and line. The app icon and the small image are hidden. A Live Activity is a labelled group; its title is not a live region, so a ticking countdown is not announced every second. The track is a progressbar with its value.
Focus
A PushPhone is a labelled group; its status bar, clock, wallpaper and quick settings are hidden, so only what you put inside is read. Every button has a 2px primary ring. The Android chevron reports aria-expanded.
Contrast and colour
Every text pair is 4.5:1 or more against the lightest background it can sit on (table above). Urgency is never colour alone: the last-hour Live Activity also changes its words.
Touch and pointer
This component previews what the system draws; the real notification's touch targets are the system's. In the preview the Android chevron is 24 × 24 and the actions are 36 tall.
You must: keep the title and line meaningful on their own, because the system may truncate them, and make sure the destination exists when the notification is tapped.
Figma mapping
There is no Figma frame for this component. It was designed in code from the owner's two references and reviewed on device sizes; Figma stays read-only.
Guidelines and related components
Use Push notification for
- Previewing a notification before it is built
- Notification settings screens that show what each kind looks like
- Documentation and design review
Don't use it for
- A message inside the app (use Alert or Toast)
- Sending or scheduling anything (that is the notification service)
- A custom, branded card on the lock screen: the system draws the real one