Morphing Island Notch
A status pill that resizes itself fluidly around whatever content it holds.
- Category
- Components
- Added
- 2026-09-20
- Deps
- 1
- Updated
- 2026-09-20
Props
"use client";
import { AnimatePresence, motion } from "framer-motion";
import type { ReactNode } from "react";
export function MorphingIslandNotch({
icon,
label,
detail,
stiffness = 420,
dark = true,
}: {
icon: ReactNode;
/** When label is null the island collapses to its compact pill. */
label?: string | null;
detail?: string | null;
stiffness?: number;
/** Dark island on a light page, or the inverse. */
dark?: boolean;
}) {
const expanded = label != null;
return (
<motion.div
/*
* No width or height is ever specified. `layout` makes Framer measure
* the element's own content and animate its box to match, so adding a
* new state is just adding markup — no width tables, no measuring.
*/
layout
transition={{ type: "spring", stiffness, damping: 34, mass: 0.9 }}
/*
* borderRadius must be ANIMATED, not left as a static class: Framer's
* layout projection scales the box, which skews a fixed corner radius
* while the size is changing. Animating it keeps the corners true.
*/
animate={{ borderRadius: expanded ? 20 : 999 }}
className={
"flex items-center overflow-hidden " +
(dark ? "bg-[#0a0c10] text-white " : "bg-white text-[#0a0c10] ") +
(expanded ? "gap-3 px-4 py-3" : "gap-0 px-3 py-3")
}
>
<motion.span layout="position" className="shrink-0">
{icon}
</motion.span>
<AnimatePresence initial={false} mode="popLayout">
{expanded && (
<motion.div
key={label}
layout="position"
initial={{ opacity: 0, filter: "blur(4px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(4px)" }}
transition={{ duration: 0.18 }}
className="flex min-w-0 flex-col"
>
<span className="whitespace-nowrap font-mono text-[9px] uppercase tracking-[0.16em] opacity-60">
{label}
</span>
<span className="whitespace-nowrap text-[13px] font-semibold">
{detail}
</span>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
About this component
Most implementations of a morphing status island hard-code a width per state, which works for the three states in the demo and falls apart the moment the content is dynamic — a longer track title or a translated string breaks the layout. Putting `layout` on the container and specifying no dimensions at all hands the measurement to Framer: it reads the element's natural size before and after, then animates the box between them. Adding a state becomes adding markup. The detail that separates a convincing version from a janky one is the corner radius. Framer implements layout animation by scaling the box, and a scaled box skews a static `border-radius` into an ellipse for the duration of the transition — animating the radius alongside the layout change keeps the corners circular the whole way. Inner elements use `layout="position"` so they translate into place rather than being stretched by the parent's inverse-scale correction. Since this usually reports state changes, the live region belongs on whatever owns the status, not on the decorative shell.
Curator’s note
Built for viberdy 2.0. Letting Framer measure the content instead of maintaining per-state widths is what makes this extensible rather than a demo.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Expandable Search Bar
componentsA search icon that smoothly expands into a full text input on click.
FLIP Grid Reorder
animationsA filterable grid whose tiles travel to their new positions instead of snapping.
Orbital Action Menu
componentsA radial menu that fans actions along an arc, evenly spaced at any count.