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

PartRequiredNotes
App icon / colored iconYesIdentifies the source at a glance. Use the app icon for system notifications; a category icon (filled) for in-app banners.
App nameYesSmall uppercase label. Gives context before the user reads the title.
TimestampRecommended"now", "2m ago", or an absolute time. Omit only if the banner is ephemeral and timing is always 'now'.
TitleYesOne line, ≤50 chars. The most important information — often the sender name or event.
Body / subtitleRecommendedPreview of the content. 1–2 lines max. Truncate with ellipsis if longer.
Action buttonsNoLong-press affordance on iOS (UNNotificationAction). In-app banners can show up to 2 inline buttons for quick actions.

Notification types

TypeUse caseUrgencyAuto-dismiss
MessageNew chat, reply, mentionMedium3–5 s
SocialLike, follow, commentLow3–5 s
AlertLow battery, storage, connectivityHighNever (sticky)
ReminderCalendar event, task dueMedium5 s
PromoSale, new content, featureNone3–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.