CoreReveals and Continuous Motionmotion/react

PortalRevealPanel

Clip-path iris and geometric portal entrance animation supporting multiple origin coordinates, staggered child reveals, and clean unmount choreography.

Preview

origin: center

Radial Clip-Path Iris Portal

Clip-Path Compositor
Staggered Children
Clean Unmount

Customize

5 props
0.65
0.08

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add portal-reveal-panel

Writes components/ui/portal-reveal-panel.tsx and components/styles/portal-reveal-panel.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { PortalRevealPanel } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <PortalRevealPanel />6  );7}

Props

PropTypeDefault
title

Panel title.

string'Radial Clip-Path Iris Portal'
origin

Origin coordinate for the circular clip-path expansion.

'center' | 'top-left' | 'bottom-right' | 'top-right''center'
duration

Duration in seconds for the clip-path transition.

number0.65
staggerChildren

Delay in seconds between inner child reveals.

number0.08
isOpen

Controlled open/mount state.

booleantrue

Accessibility

Links the toggle trigger to the revealed region via aria-expanded and aria-controls, and falls back to an instant opacity toggle when prefers-reduced-motion is active.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/portal-reveal-panel.tsx
1"use client";2 3import * as React from "react";4import { motion, AnimatePresence, useReducedMotion } from "motion/react";5import styles from "../styles/portal-reveal-panel.module.css";6 7export interface PortalRevealPanelProps {8  /** Panel heading */9  title?: string;10  /** Clip-path origin coordinate */11  origin?: "center" | "top-left" | "bottom-right" | "top-right";12  /** Reveal duration in seconds */13  duration?: number;14  /** Child stagger delay in seconds */15  staggerChildren?: number;16  /** Initial or controlled open state */17  isOpen?: boolean;18  /** Optional className */19  className?: string;20}21 22const ORIGIN_MAP: Record<string, string> = {23  center: "50% 50%",24  "top-left": "8% 12%",25  "bottom-right": "92% 88%",26  "top-right": "92% 12%",27};28 29export function PortalRevealPanel({30  title = "Radial Clip-Path Iris Portal",31  origin = "center",32  duration = 0.65,33  staggerChildren = 0.08,34  isOpen: isOpenProp = true,35  className = "",36}: PortalRevealPanelProps) {37  const [open, setOpen] = React.useState(isOpenProp);38  const prefersReducedMotion = useReducedMotion();39  const regionId = React.useId();40 41  React.useEffect(() => {42    setOpen(isOpenProp);43  }, [isOpenProp]);44 45  const coord = ORIGIN_MAP[origin] ?? "50% 50%";46 47  return (48    <div className={`${styles.wrapper} ${className}`.trim()}>49      <div className={styles.toolbar}>50        <button51          type="button"52          aria-expanded={open}53          aria-controls={regionId}54          onClick={() => setOpen((prev) => !prev)}55          className={styles.toggleBtn}56        >57          {open ? "Close Portal Panel" : "Open Portal Panel"}58        </button>59        <span className={styles.originBadge}>origin: {origin}</span>60      </div>61 62      <AnimatePresence mode="wait">63        {open && (64          <motion.div65            id={regionId}66            key={origin}67            initial={68              prefersReducedMotion69                ? { opacity: 0 }70                : { clipPath: `circle(0% at ${coord})`, opacity: 0.4 }71            }72            animate={73              prefersReducedMotion74                ? { opacity: 1 }75                : { clipPath: `circle(150% at ${coord})`, opacity: 1 }76            }77            exit={78              prefersReducedMotion79                ? { opacity: 0 }80                : { clipPath: `circle(0% at ${coord})`, opacity: 0 }81            }82            transition={{ duration, ease: [0.16, 1, 0.3, 1] }}83            className={styles.panel}84          >85            <h4 className={styles.title}>{title}</h4>86            <div className={styles.metricsGrid}>87              {["Clip-Path Compositor", "Staggered Children", "Clean Unmount"].map(88                (item, idx) => (89                  <motion.div90                    key={item}91                    initial={prefersReducedMotion ? false : { opacity: 0, y: 12 }}92                    animate={{ opacity: 1, y: 0 }}93                    transition={{ delay: idx * staggerChildren + 0.12, duration: 0.35 }}94                    className={styles.chip}95                  >96                    {item}97                  </motion.div>98                )99              )}100            </div>101          </motion.div>102        )}103      </AnimatePresence>104    </div>105  );106}