Skip to content

Selvedge Blur

A progressive blur along one edge of a scrolling area or image: stacked layers of backdrop blur that fade from sharp to blurred, so content softens as it nears the edge. Pure decoration: it ignores the pointer and is hidden from assistive technology.

Motionv0.1.0experimentalWCAG 2.2 AAView spec

Optional, and kept apart.

Nothing here is included in a project unless you add it, and the plain component it builds on is unchanged. This effect is decoration: it is hidden from screen readers, stands still for visitors who prefer reduced motion, and can be paused. Use Still on a preview to see that version.

  • Quarterly review notes
  • Invoices sent this week
  • Stock reserved for orders
  • Payments waiting on approval
  • Refunds issued today
  • New accounts created
  • Support tickets closed
import { SelvedgeBlur } from "@rdloom/react";

const lines = ["Quarterly review notes", "Invoices sent this week", "Stock reserved for orders", "Payments waiting on approval", "Refunds issued today", "New accounts created", "Support tickets closed"];

// The blur sits on the bottom edge of a positioned parent and softens what runs under it.
export default function SelvedgeBlurBasicExample() {
  return (
    <div className="flex w-full justify-center">
      <div className="relative h-48 w-72 overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] p-4">
        <ul className="m-0 flex list-none flex-col gap-3 p-0 text-sm">
          {lines.map((line) => (
            <li key={line}>{line}</li>
          ))}
        </ul>
        <SelvedgeBlur position="bottom" />
      </div>
    </div>
  );
}

Installation

npx rdloom add selvedge-blur

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

It also adds rdloom-motion.css next to your tokens and imports it from the tokens file, once: no setup, and no inline styles. The plain components you already use are untouched.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/selvedge-blur.json

Works, but without upgrade tracking, and you import the motion CSS yourself.

Usage

import { SelvedgeBlur } from "@rdloom/react";

<div className="flex w-full justify-center">
  <div className="relative h-48 w-72 overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] p-4">
    <ul className="m-0 flex list-none flex-col gap-3 p-0 text-sm">
      {lines.map((line) => (
        <li key={line}>{line}</li>
      ))}
    </ul>
    <SelvedgeBlur position="bottom" />
  </div>
</div>

Scroll edges

  • Activity entry 1
  • Activity entry 2
  • Activity entry 3
  • Activity entry 4
  • Activity entry 5
  • Activity entry 6
  • Activity entry 7
  • Activity entry 8
  • Activity entry 9
  • Activity entry 10
  • Activity entry 11
  • Activity entry 12
  • Activity entry 13
  • Activity entry 14
import { SelvedgeBlur } from "@rdloom/react";

const items = Array.from({ length: 14 }, (_, i) => `Activity entry ${i + 1}`);

// Blur both ends of a scrolling list. The list stays fully scrollable and clickable underneath.
export default function SelvedgeBlurScrollEdgesExample() {
  return (
    <div className="flex w-full justify-center">
      <div className="relative w-72 rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
        <div tabIndex={0} aria-label="Recent activity" role="region" className="h-48 overflow-auto p-4 outline-none focus-visible:ring-2 focus-visible:ring-[var(--rd-color-focus-ring)]">
          <ul className="m-0 flex list-none flex-col gap-3 p-0 text-sm">
            {items.map((item) => (
              <li key={item}>{item}</li>
            ))}
          </ul>
        </div>
        <SelvedgeBlur position="top" size="20%" strength={6} />
        <SelvedgeBlur position="bottom" size="20%" strength={6} />
      </div>
    </div>
  );
}

Horizontal

DesignBuildShipMeasureLearnRepeatReviewPlan
import { SelvedgeBlur } from "@rdloom/react";

const tags = ["Design", "Build", "Ship", "Measure", "Learn", "Repeat", "Review", "Plan"];

// start and end follow the reading direction, so this also works in right-to-left layouts.
export default function SelvedgeBlurHorizontalExample() {
  return (
    <div className="flex w-full justify-center">
      <div className="relative w-80 overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] p-4">
        <div className="flex w-[34rem] gap-3 text-sm">
          {tags.map((tag) => (
            <span key={tag} className="rounded-full border border-[var(--rd-color-border-default)] px-3 py-1">
              {tag}
            </span>
          ))}
        </div>
        <SelvedgeBlur position="start" size="20%" strength={6} />
        <SelvedgeBlur position="end" size="20%" strength={6} />
      </div>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
position

Which edge of the parent is blurred. start and end follow the reading direction.

"top" | "bottom" | "start" | "end""bottom"
strength

Blur radius in px at the very edge.

number8
size

How far the blur reaches in from the edge, any CSS length.

string"30%"
layers

Number of stacked blur layers. More layers give a smoother fade.

number5

Accessibility

Role presentation, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

Not interactive.

Screen readers announce

  • Not announced

What your code must do

  • Decoration only: aria-hidden, never focusable, and pointer-events none so the content below stays clickable and selectable
  • It does not move, so there is nothing to pause; the blur is the same for visitors who prefer reduced motion
  • It only softens what is under it: do not put content the reader needs to read inside the blurred zone
  • In forced-colors (high contrast) mode the blur is removed, so text is never obscured
  • Place it inside a positioned parent (position: relative), above the content in stacking order

Guidelines

Use it when

  • The edge of a scrolling list or feed that continues off screen
  • The bottom of a card with an image behind text

Avoid it when

  • Over text that must stay readable
  • Where backdrop-filter is not wanted for performance reasons on long pages

Don't

  • Covering interactive controls, which become hard to see
  • Using a large strength with a tiny size, which looks like a hard band