Back to Devoured - August 31, 2026
DEVOURED
On Unfurling Interfaces

On Unfurling Interfaces

Design Karlkoch
Small UI elements work best when they unfold from their source rather than fading, creating a clear spatial relationship between trigger and target.
What: Karl Koch describes a method for "unfurling" interface components using CSS `clip-path: inset()` to animate from a narrow seam into a full component. He emphasizes keeping native browser behaviors (like Popover API) while using shared motion language to establish visual continuity.
Why it matters: Design systems often over-rely on generic fade animations that erase the spatial context of an interface. Using native primitives alongside controlled clip-path transitions maintains performance and keeps components tethered to their functional origins.
Takeaway: Replace fade-in transitions for tooltips or menus with `clip-path` animations that originate from the trigger element to better signal spatial hierarchy.
Deep dive
  • Spatial Anchoring: UI surfaces should inherit location from the trigger to clarify context for the user.
  • CSS Implementation: Use clip-path: inset() with round corners to ensure the component remains visually consistent during transitions.
  • Native Lifecycle: Prioritize the Popover API for top-layer management, focus, and dismissal rather than building custom state logic.
  • Shared Grammar: Shared motion should be limited to common easing and naming conventions rather than bundling into one monolithic, over-engineered component.
  • Edge Cases: Always test reduced-motion preferences, keyboard accessibility, and behavior on touch devices where native hover/click patterns differ.
Decoder
  • Popover API: A native browser feature that handles positioning, top-layer layering, and light-dismissal for UI elements without requiring complex z-index management.
  • Clip Path: A CSS property that masks an element by creating a specific shape; used here to reveal content incrementally rather than via opacity.
  • Stacking Context: The browser's three-dimensional conceptual model that determines which elements are drawn on top of others.
Original article

Most small interface surfaces arrive the same way: a fade and a scale.

It’s a reasonable default: a menu, toast or hover detail is there one moment and visible the next. The implementation is short, works across components, and rarely looks actively bad.

It also rarely explains itself.

I wanted the small interactive surfaces on this site to feel related without turning the site into an animation demo. The toast should feel like it came from navigation, rather than appearing in vacant screen space. The booking menu should feel attached to Book a chat. A project detail should read as part of its card, not a label laid over an image after the fact.

The rule I landed on was: a surface should unfold from the edge or control that caused it.

Not every interaction needs a journey, but the ones that introduce a new local surface benefit from a visible origin.

Start with the relationship, not the effect

“Use a clip path” is an implementation decision, not the design decision.

The design decision is spatial: if one thing reveals another, the second thing should inherit a location from the first. That location can be a trigger, an edge, or an enclosing card. The motion then makes a claim the rest of the interface can keep: these two things belong together.

On this site there are three versions of that relationship:

  1. Feedback rail: a confirmation unfolds from the desktop navigation or mobile bottom rail.
  2. Anchored menu: booking options unfold from the Book a chat control.
  3. Card detail: a project caption unfolds from the lower edge of its image.

They don’t have identical geometry. A toast is a pill; a menu is a rounded surface; a card detail is inset within a frame. They share the same grammar: start narrow at the anchor, expand to the real surface, keep the material intact.

That’s more useful than a global fade-in utility. The class of interaction is shared; the geometry still belongs to the component.

A seam is enough

The closed state is a narrow seam, not a tiny scaled-down panel.

For a surface that unfolds down from a header, I use an inset clip path:

.motion-unfold-menu {
  clip-path: inset(0 42% 100% round var(--radius-menu));
  transform: translateY(-0.375rem);
}

.motion-unfold-menu[data-open] {
  clip-path: inset(0 round var(--radius-menu));
  transform: translateY(0);
}

The horizontal inset leaves a thin centre section visible in the conceptual starting state, while the bottom inset hides the whole height. When the menu opens, the clip expands in both directions and the small vertical offset settles into place.

The result is less theatrical than a radial reveal and more specific than a fade. It has direction without pretending that a menu is a physical object flying around the page.

For the card caption, the seam starts along the lower edge because that’s where the caption rests. For the mobile feedback rail, it reverses: the confirmation unfolds upward from the bottom navigation. Same idea, different anchor.

The radius matters: if the clip path doesn’t use the surface’s actual radius, the corners briefly square off during the reveal. That tiny material break is the kind of thing people may not name, but they notice it as “cheap”. Motion isn’t separate from borders, shadows and radii; it’s those things changing state.

Share the language, not the component

My first version tried to route every surface through one clever morph component, but I didn’t keep it.

A toast lives in a stacked viewport and needs a rail-specific direction, timing and z-index relationship. A native popover has top-layer behaviour, focus management and browser-owned positioning. A card caption is ordinary document content that should remain permanently visible on touch devices.

They shouldn’t share lifecycle code just because they share an aesthetic.

Instead, the shared layer is deliberately small:

  • a common easing curve
  • a short entry duration
  • a faster, quieter exit where the platform supports one
  • clip paths that preserve each surface’s radius
  • a naming convention for an anchored, unfurling surface

The component owns its placement and accessibility. The shared language owns how a local surface arrives.

This is a useful boundary for design systems. Reuse a decision only at the level where it’s actually the same decision. A universal component can erase the meaningful differences between a menu, a notification and a card. A shared motion rule can keep their family resemblance without doing that.

Native behaviour gets the final say

The booking menu uses a native popover through the component library. That buys a lot: top-layer rendering, light-dismiss, Escape behaviour, focus handling and anchor positioning. The browser is better placed to manage that contract than a pile of click-outside listeners and z-index: 9999.

It also means the browser owns part of the component’s lifecycle.

My first close animation assumed the menu would keep its open state while it collapsed back into a seam. In a recording, it briefly shifted before disappearing. The visual bug was small but obvious once you knew where to look: the surface was trying to animate after the native popover had released the geometry tying it to its trigger.

The entry remains expressive while the native close is atomic. There’s no detached panel available to flash in a default position.

That’s an engineering decision in service of the design. A beautiful intended exit is worse than a quiet, correct disappearance. Keep the relationship believable.

Test the part a screenshot can’t show

Motion work is unusually easy to approve from static code.

The tokens are right. The duration looks reasonable. The component type-checks. A screenshot of the settled state has rounded corners and the right shadow. None of that proves the interaction works.

For this pass I checked the actual pointer interaction in the browser: an opening frame, a settled frame, repeated opens and closes, and the reduced-motion rules. When the exit looked suspect, I inspected a screen recording frame by frame with ffmpeg instead of guessing from a replay, which changed the implementation.

The useful review questions weren’t “does it animate?” They were:

  • Does the surface have an obvious origin?
  • Does it preserve its radius, shadow and position while moving?
  • Does it stay attached to the thing that caused it?
  • Is the exit shorter and less demanding than the entry?
  • Does the control still work with keyboard and reduced motion?
  • What happens on the second and third interaction, not only the first?

The visual intent creates the test. The platform lifecycle decides whether the implementation can keep that intent. The browser decides whether the claim was true.

Restraint is part of the layer

The same language doesn’t belong everywhere.

Page navigation already has its own rhythm. Text fields need immediate clarity, not an unveiling ceremony. Dense lists often need to update without every row introducing itself. A regular hover colour change doesn’t become more meaningful because it’s clipped from a seam.

I use the unfurl where a new local surface appears and the source of that surface is visible. It should make the interaction easier to read, not announce that the designer found a new CSS property.

The best outcome is modest: a confirmation feels like it came from the navigation you just used, a menu feels attached to its button, and a project caption feels built into the card. Nobody needs to describe the motion back to me for it to have done its job.

When to steal this

Use an unfurling surface when an interaction reveals a contained piece of UI with a clear spatial cause:

  • menus and small action panels
  • inline confirmations
  • card details and previews
  • contextual controls
  • small pickers or filters

Start with the anchor. Decide which edge the surface should emerge from. Keep the motion short, preserve the material tokens during the transition, and test the exit in the real browser.

If the surface can’t stay visually tethered to its cause, simplify it. A quiet disappearance is better than a clever animation that breaks the relationship it was meant to explain.

Explore all 65 articles in today's digest

Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi