Mobile Notifications
iOS-style banner notifications that slide in from the top of the screen. Used for real-time alerts, messages, and system events. Banners auto-dismiss after a few seconds; they never block interaction with the app behind them.
Preview
9:41
Tap a type below
Banner slides in from the top and auto-dismisses after 3.5 s
Tap a type to trigger the banner. It auto-dismisses after 3.5 s.
Anatomy
| Part | Required | Notes |
|---|---|---|
| App icon / colored icon | Yes | Identifies the source at a glance. Use the app icon for system notifications; a category icon (filled) for in-app banners. |
| App name | Yes | Small uppercase label. Gives context before the user reads the title. |
| Timestamp | Recommended | "now", "2m ago", or an absolute time. Omit only if the banner is ephemeral and timing is always 'now'. |
| Title | Yes | One line, ≤50 chars. The most important information — often the sender name or event. |
| Body / subtitle | Recommended | Preview of the content. 1–2 lines max. Truncate with ellipsis if longer. |
| Action buttons | No | Long-press affordance on iOS (UNNotificationAction). In-app banners can show up to 2 inline buttons for quick actions. |
Notification types
| Type | Use case | Urgency | Auto-dismiss |
|---|---|---|---|
| Message | New chat, reply, mention | Medium | 3–5 s |
| Social | Like, follow, comment | Low | 3–5 s |
| Alert | Low battery, storage, connectivity | High | Never (sticky) |
| Reminder | Calendar event, task due | Medium | 5 s |
| Promo | Sale, new content, feature | None | 3–4 s |
Implementation
MobileNotification.tsx
tsx
// iOS-style banner notification inside a phone frame
// For real apps, use the OS notification API (Expo Notifications / FCM)
import { useState, useEffect } from "react";
import { MessageCircle } from "lucide-react";
import { cn } from "@/lib";
interface BannerProps {
visible: boolean;
icon: React.ReactNode;
app: string;
title: string;
body: string;
onDismiss: () => void;
duration?: number; // ms, default 3500
}
export function NotificationBanner({
visible, icon, app, title, body, onDismiss, duration = 3500,
}: BannerProps) {
useEffect(() => {
if (!visible) return;
const t = setTimeout(onDismiss, duration);
return () => clearTimeout(t);
}, [visible, duration, onDismiss]);
return (
<div
className={cn(
"fixed top-safe-top left-0 right-0 z-50 px-3 pt-2 transition-transform duration-300",
visible ? "translate-y-0" : "-translate-y-full"
)}
>
<div className="flex items-start gap-2.5 px-3 py-2.5 rounded-2xl
bg-surface-raised/90 backdrop-blur-sm border border-border
shadow-[0_4px_20px_rgba(0,0,0,0.25)]">
<div className="w-8 h-8 rounded-xl bg-surface flex items-center justify-center flex-shrink-0">
{icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-baseline justify-between gap-1">
<span className="text-[10px] font-semibold text-text-tertiary uppercase tracking-wide truncate">{app}</span>
<span className="text-[10px] text-text-tertiary flex-shrink-0">now</span>
</div>
<p className="text-[12px] font-semibold text-text-primary truncate">{title}</p>
<p className="text-[11px] text-text-secondary truncate">{body}</p>
</div>
</div>
</div>
);
}
// Usage
export function Example() {
const [visible, setVisible] = useState(false);
return (
<>
<button onClick={() => setVisible(true)}>Trigger notification</button>
<NotificationBanner
visible={visible}
icon={<MessageCircle className="w-4 h-4 text-blue-400" />}
app="Messages"
title="Jamie replied to you"
body="Sounds good, see you at 3pm!"
onDismiss={() => setVisible(false)}
/>
</>
);
}Design decisions
- →Banners never block interaction. The banner sits above content without a blocking overlay. Users can interact with the app while the banner is visible. Never use a full-screen overlay for a notification.
- →Frosted glass surface. backdrop-filter: blur(12px) with a semi-transparent surface lets users see context behind the banner — they don't lose their place in the app. Ensure enough opacity for contrast in both light and dark modes.
- →Safe area clearance. Position banners at env(safe-area-inset-top) on iOS to avoid the Dynamic Island and status bar. Use a top-padding of ~8px after the safe area.
- →Duration tuned to content length. Short titles (under 40 chars) can dismiss in 3 s. Longer content or alerts should stay 5 s or be sticky. Users reading slowly should not have content yanked away.
- →Sound + haptics. System notifications pair with a sound. In-app banners should not auto-play sound — use haptics (UIFeedbackGenerator / Haptics API) for subtle acknowledgement instead.
Accessibility
- →Use role="alert" with aria-live="assertive" for urgent notifications. For lower-priority banners use aria-live="polite" so the announcement waits for a natural pause.
- →aria-atomic="true" ensures the entire banner content is read as one unit when it appears, rather than piece by piece.
- →Do not auto-dismiss notifications that require user action (errors, critical alerts). A user who steps away from the screen should not miss an actionable state.
- →Reduce Motion: Respect prefers-reduced-motion by removing slide animation. The banner can still appear/disappear instantly — just without the translate transition.
- →VoiceOver / TalkBack: The notification should be announced immediately on appearance. Test that the app name, title, and body are all read in order.