On unfurling interfaces

I wanted the smaller surfaces on this site to feel like they belonged to the thing that opened them. A fade and a little scale works well enough in plenty of places, but it wasn’t giving me much of that connection here.

The toast needed to feel connected to the navigation, the booking menu to Book a chat, and the project detail to its card. I wanted those relationships to be visible without making every interaction into a big event.

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

Finding the origin

Before choosing an effect, I worked out where each surface should appear from. Sometimes that was the control itself, sometimes the edge of the area around it. Once I had that point, I could try a reveal that kept the two things connected.

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.

The shapes are different, but I can use a similar approach for each: start with a narrow opening at the anchor, then expand it into the full surface whilst keeping the corners intact.

A seam is enough

I start the closed state as a narrow seam, using the clip path to hide the rest of the 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.

That gives the opening a direction without needing to move the whole panel very far.

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.

I also need the clip path to use the surface’s radius, otherwise the corners briefly square off during the reveal. It’s easy to miss in the final state, but once I noticed it during the movement I couldn’t really leave it there.

Working out what to share

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.

Those differences made the shared lifecycle harder to justify. The components could look related without having to open, close and manage their content in exactly the same way.

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

Each component still handles its placement and accessibility, with those few motion decisions shared between them.

That ended up being a more useful boundary for the system. I could change the common timing whilst leaving the menu, toast and caption free to deal with their own behaviour.

Native behaviour gets the final say

The booking menu uses a native popover through the component library, so the browser handles the top layer, light-dismiss and Escape behaviour. It also gives me the focus and positioning behaviour I need to work with.

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.

I kept the entry animation and let the native close happen immediately. That removed the moment where the panel could appear detached from its trigger.

I liked the idea of the closing animation, but the simpler version worked better in the actual interaction.

On anchored interfaces goes deeper on why the browser’s popover and anchor primitives are worth using in the first place.

Here are the three decisions together. Every panel uses the same content, so the difference comes entirely from its relationship to the trigger, its material during the reveal, and how it leaves.

Interactive demo —

Test the part a screenshot can’t show

The code can look perfectly reasonable whilst a transition still feels wrong. A screenshot of the final state won’t show me the corner changing shape halfway through, or the little jump just before a panel disappears.

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.

These were the questions I used whilst watching it:

  • 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?

Knowing where to leave it alone

I don’t want the same reveal everywhere. A text field needs to be ready to use, and a dense list can get quite busy if every row has its own arrival. For an ordinary hover state, a colour change may already be doing enough.

I’m using the unfurl where there’s a new local surface and a visible place for it to come from. That’s where it seems to help me follow what just happened.

Trying it elsewhere

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.

And keep trying the close as well as the open. That was where my version needed the most work, even though the entry was the part I’d spent more time thinking about.

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论