Clip-path iris and geometric portal entrance animation supporting multiple origin coordinates, staggered child reveals, and clean unmount choreography.
Preview
Radial Clip-Path Iris Portal
Clip-Path Compositor
Staggered Children
Clean Unmount
Customize
5 props0.65
0.08
Installation
Copies the component source and its CSS Module into your project.
npx motionfoundry add portal-reveal-panelWrites 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
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}