Motion+

iOS Notifications stack

A iOS inspired notifications stack using Motion for React's variants.

React
Tutorial time
5 min
Difficulty

Tutorial

Introduction

The iOS Notifications Stack example shows how to create an expandable stack of notification cards with smooth spring animations. When collapsed, the notifications are stacked on top of each other with a subtle scaling effect. When expanded, they spread out for better visibility, with a header appearing at the top.

In this tutorial, we'll learn to use several Motion for React APIs to create this effect:

Get started

Let's begin by setting up the basic structure and some constants for our notifications stack:

"use client"

import { CSSProperties, useState } from "react"

const N_NOTIFICATIONS = 3
const NOTIFICATION_HEIGHT = 60
const NOTIFICATION_WIDTH = 280
const NOTIFICATION_GAP = 8

export default function NotificationsStack() {
    const [isOpen, setIsOpen] = useState(false)

    return (
        <div style={stackStyle}>
            <Header isOpen={isOpen} onClose={() => setIsOpen(false)} />

            {Array.from({ length: N_NOTIFICATIONS }).map((_, i) => (
                <Notification
                    key={i}
                    index={i}
                    onClick={() => {
                        setIsOpen((open) => !open)
                    }}
                />
            ))}
        </div>
    )
}

const Header = ({
    isOpen,
    onClose,
}: {
    isOpen: boolean
    onClose: () => void
}) => {
    return (
        <div style={headerStyle}>
            <h2 style={headerTitleStyle}>Notifications</h2>
            <button style={headerCloseStyle} onClick={onClose}>
                Collapse
            </button>
        </div>
    )
}

const Notification = ({
    index,
    onClick,
}: {
    index: number
    onClick: () => void
}) => {
    const notificationStyle = getNotificationStyle(index)

    return <div style={notificationStyle} onClick={onClick} />
}

/**
 * ==============   Styles   ================
 */
const stackStyle: CSSProperties = {
    position: "relative",
    display: "flex",
    flexDirection: "column",
    gap: NOTIFICATION_GAP,
}

const headerStyle: CSSProperties = {
    position: "absolute",
    top: -40,
    left: 0,
    height: 28,
    width: "100%",
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    transformOrigin: "bottom center",
    pointerEvents: "none",
}

const headerTitleStyle: CSSProperties = {
    fontSize: 18,
    lineHeight: 1,
    marginLeft: 8,
}

const headerCloseStyle: CSSProperties = {
    fontSize: 14,
    lineHeight: 1,
    marginRight: 8,
    backgroundColor: "#1f1f1f",
    padding: "4px 12px",
    height: 20,
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: 16,
    cursor: "pointer",
    pointerEvents: "auto",
    userSelect: "none",
}

/**
 * ==============   Utils   ================
 */
function getNotificationStyle(index: number): CSSProperties {
    return {
        height: NOTIFICATION_HEIGHT,
        width: NOTIFICATION_WIDTH,
        backgroundColor: `#ffffff`,
        borderRadius: 16,
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        zIndex: N_NOTIFICATIONS - index,
        userSelect: "none",
    }
}

This code creates the basic structure of our component:

  1. A parent component NotificationsStack that manages the open/closed state
  2. A Header component that appears when the stack is expanded
  3. Multiple Notification components that will be animated

Related examples

Latest in React

Motion+

Unlock all 400+ examples

  • Source code for every Plus example.
  • Provide examples direct to your agent via Motion's MCP.
  • Lifetime access to new examples and APIs.