Skip to content

Edge Fades over Hard Clipping

A UI principle for coding agents. Also covers masking, overflow, clipping, edge fade, container bleed, mask-image, and 6 more.

Show all 12 aliases

masking, overflow, clipping, edge fade, container bleed, mask-image, content touches the border during animation, text overlaps the rounded corner mid-transition, should i use overflow hidden, dropdown content escapes its panel, carousel items collide with the frame, outermost column looks dimmed

When content moves inside a bounded container, fade it at the edges instead of clipping it. Put the gradient mask on a wrapper that spans the container's padding, not on the content itself.

The Failure

A dropdown, tab panel, or carousel animates its content: a slide, a scale, a staggered reveal. During the transition the moving content passes outside the container's content box and collides with the border or rounded corner. Text sits on top of the frame for 200-400ms.

It looks like a rendering bug, and it reads as one even to people who can't say what's wrong. It is invisible in static design files, which is why it survives review.

Why overflow: hidden Is the Wrong Reflex

Clipping produces a hard edge. Content moving toward it appears to be cut in half, not to pass behind the frame. The animation is what the container was built to showcase, and clipping is the one thing that visibly damages it.

Clipping also changes the box itself. overflow: hidden establishes an independent formatting context, and once content overflows it makes the element a scroll container. Nothing escapes the padding box after that, so focus rings, tooltips, submenus, and hover targets that extend past the panel are all cut off. The clipped content still exists and is still reachable: tab to a focusable element inside the hidden region and the browser scrolls it into view, so a panel you thought was static silently jumps.

It does not create a stacking context. If you reached for it to fix a layering problem, it was never the lever.

overflow: clip skips both the scroll container and the formatting context, so prefer it when a hard cut is genuinely what you want. It still gives you the hard edge this principle exists to avoid.

The Pattern

.panel-viewport {
  position: relative;
  margin-inline: calc(-1 * var(--panel-padding));
  padding-inline: var(--panel-padding);

  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--panel-padding),
    #000 calc(100% - var(--panel-padding)),
    transparent 100%
  );
  mask-size: 100% 100%;
  mask-repeat: no-repeat;
}

The negative margin cancels the container's padding so the viewport spans edge to edge; the matching padding puts the content back at its original inset. The result is that the mask's fade ramp lands exactly on the padding gutter, a strip that is empty at rest.

Content is fully opaque until it starts moving, dissolves as it crosses into the gutter, and is gone before it reaches the border.

The mask color is irrelevant here because alpha is what masks, so #000 is exempt from the oklch-only rule.

Sizing the Fade

Match the ramp to the container's padding. This is not aesthetic tuning, it is what keeps the fade off resting content.

  • Ramp shorter than the padding: the fade covers only the strip nearest the border, and the rest of the gutter stays fully opaque. Moving content crosses that opaque strip at full strength and then dissolves abruptly right against the frame, which is the hard cut you were trying to avoid.
  • Ramp longer than the padding: the fade extends over resting content and the outermost column looks permanently dimmed. This is the mistake that makes people conclude masking doesn't work.

If the overflow distance varies with viewport, drive both the padding and the mask stops from one custom property so they can't drift.

Where the Mask Goes

On the positioning context, never on the element that transforms. A mask box travels with its element's transform, so masking the animating node means the fade slides off the container edge mid-animation, the exact moment it was supposed to be doing its job.

The mask box also bounds what stays visible. With mask-repeat: no-repeat, everything outside the mask's own box is masked away completely, so a mask lets content escape the container no more than clipping does. Anything that must paint past the edge, a tooltip, a submenu, an open popover, belongs in a portal or an unmasked sibling, outside the masked subtree.

Do not use the fade to take content out of play. Whether masked-out pixels stay clickable is an unsettled interop split: the spec says masking does not affect hit testing and WebKit follows that, while Chrome and Firefox currently do consult the mask. Keep pointer targets, focusable controls, and their focus indicators out of the ramp rather than depending on either behavior, and confirm it against the browsers you support.

Applies To

Nav dropdowns with sliding panels, tab content that transitions horizontally, carousels and marquees, scrollable lists that should fade under a sticky header, command palettes with animated result sets, and any container whose children animate near a rounded border.

Vertical cases use to bottom with the block padding. For scroll-linked fades that animate the stops via @property and animation-timeline: scroll(), see the scroll-linked fade effect in animation-and-motion.md.

Review Checks

  • Can a transformed child actually reach the container's edge or rounded corner during the transition? A transform alone is not the trigger; the crossing is.
  • Scrub the transition at 10% speed. Does content touch the border at any frame?
  • At rest, is the outermost content the same opacity as the rest? If not, the ramp is too long.
  • Remove the mask and scrub the transition again. If nothing bleeds at any frame, the mask is decorative, so delete it. Judging this at rest tells you nothing.

Anti-Patterns

  • Masking the animating element rather than its wrapper.
  • overflow: hidden on a container whose children need to escape it.
  • Reaching for a fade on a container where nothing moves. Static overflow is a layout bug; fix the layout.
  • Painting a gradient-filled pseudo-element over the edge instead of masking. It only works on a known solid background and breaks over imagery or translucency.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-ui-principle({ topic: "edge-fades" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems