Add this component

npx zentauri-ui add scroll-expand

Accessibility notes

Keyboard accessible by default with semantic markup, visible focus treatment, and tokenized states. Add descriptive labels for icon-only or decorative usage.

Dependency notes

Check Installation for shared peers. This component keeps styling in Tailwind classes and the --zui-* token contract.

Media & motion

Room to grow.

A rounded media frame that opens to full bleed as you scroll. The headline lifts away, your content fades in over the media, and the section holds before it releases.

Mountains under a starry sky

Every pixel, everywhere

The frame opens as you scroll and hands the whole stage to your media.

How it works

Built to scale

Your stage

Tune the expansion.

Scroll inside the preview. Use the page theme toggle for light and dark.


Every pixel, everywhere

The frame opens as you scroll and hands the stage to your media.

Built to scale

Distance and hold are measured in stage heights.

Choose an appearance. It sets the scrim, the headline and overlay text, and the hint colour.

One frame. Room to grow.

These examples scroll inside their own frame. Click or tab into one, then scroll with a wheel, a trackpad, or the arrow and Page Down keys.

A photograph that opens to full bleed


Mountains under a starry sky

Every pixel, everywhere

The frame opens as you scroll and hands the stage to your media.

Built to scale

A wide resting frame on a light scrim


A soft lounge chair in a bright room

Designed to be lived with

A lighter scrim keeps dark overlay text readable on bright media.

Sit with it

A tinted scrim, a longer hold, and a call to action


A forest path in soft light

Into the forest

A longer hold keeps the full-bleed moment on screen before the section releases.

Take the long way

Follow the page.

The default window scroller pins the section while the page scrolls past it. Keep scrolling to watch it open, hold, and release.

A forest path in soft light

Pinned to the page

With the default scroller, the section pins while the page scrolls and releases after the hold.

Scroll the page

import { ScrollExpand } from "@zentauri-ui/zentauri-components/ui/scroll-expand";

export function Showcase() {
  return (
    <ScrollExpand
      src="/forest.jpg"
      alt="A forest path in soft light"
      title="Scroll the page"
      hint="Keep scrolling"
    >
      <h3>Pinned to the page</h3>
      <p>With the default scroller, the section pins while the page scrolls and releases after the hold.</p>
    </ScrollExpand>
  );
}

Scroll drives it. Layout never moves.

The section's height comes from CSS, a tall track holding a sticky stage, so nothing shifts after the page hydrates. As you scroll, one animation loop writes only the frame's clip-path, the media zoom, and opacity, and it stops once the frame settles. Smoothing is time-based, so it feels the same at 60 Hz and 120 Hz.

The title is a real heading that labels the section. Overlay content stays inert until it is mostly visible, so keyboard users never tab into invisible links. A self-scrolling section can be focused and scrolled with the arrow and Page Down keys.

With reduced motion, the section shows its expanded composition without pinning or scroll-linked movement, and video never plays. Video otherwise plays only while the section is on screen.

Scroll Expand API

Generated from the package prop types and variant definitions.

ScrollExpand

ScrollExpandProps

Variants

PropTypeDefault
appearance
contrastdefaultlighttint
default
scroller
selfwindow
window

Behavior

PropTypeDefault
altAlternative text for an image. Leave empty when the image is decorative.string | undefined""
distanceScroll length of the expansion, in stage heights. Clamped to 0.2–5.number | undefined1.2
hintSmall cue under the resting frame that fades as scrolling begins. Hidden from assistive tech.ReactNodenone
holdExtra scroll held at full bleed before the section releases, in stage heights. Clamped to 0–5.number | undefined0.35
levelHeading level of `title`.ScrollExpandLevel | undefined2
mediaTypeKind of media `src` points to. Videos are muted, looped, and only play on screen.ScrollExpandMediaType | undefined"image"
posterPoster frame shown while a video loads.string | undefinednone
smoothingFollow time in milliseconds. 0 locks the frame to the scroll position. Clamped to 0–1000.number | undefined100
src*Image or video shown inside the frame.stringnone
startHeightFrame height before expanding, as a percentage of the stage. Clamped to 10–100.number | undefined58
startWidthFrame width before expanding, as a percentage of the stage. Clamped to 10–100.number | undefined42
titleHeadline over the resting frame. It lifts away as the media takes over and labels the section.ReactNodenone
zoomMedia zoom at rest; it eases to 1 as the frame opens. Clamped to 1–3.number | undefined1.35

Content

PropTypeDefault
childrenContent revealed over the media at full bleed. It is inert until visible.ReactNodenone
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLElement> | undefinednone
styleCSSProperties | undefinednone

ScrollExpandVariant

ScrollExpandVariantProps

Behavior

PropTypeDefault
appearance'default' | 'contrast' | 'light' | 'tint' | null | undefinednone
scroller'window' | 'self' | null | undefinednone
CSS variable overrides

Scroll Expand CSS variables

Theme the frame radius, section background, headline type, scrim gradient, overlay text and hint colour, and the focus ring of a self-scrolling section. Scroll timing is configured through props; private --_se-* values are not a public token API.

38 variables

Pattern: --zui-<component>-<slot?>-<variant?>-<property>-<state?>-dark?

:root {
  --zui-scroll-expand-radius: 24px;
  --zui-scroll-expand-bg: transparent;
  --zui-scroll-expand-focus-ring: #0369a1;
  --zui-scroll-expand-scrim: linear-gradient(to top,#020617d9,#0206171f 45%,#02061766);
  --zui-scroll-expand-fg: #ffffff;
  --zui-scroll-expand-hint: #475569;
  --zui-scroll-expand-light-scrim: linear-gradient(to top,#ffffffe6,#ffffff26 45%,#ffffff73);
  --zui-scroll-expand-light-fg: #0f172a;
  --zui-scroll-expand-light-hint: #475569;
  --zui-scroll-expand-contrast-scrim: linear-gradient(to top,#000000f2,#00000066 50%,#000000a6);
  --zui-scroll-expand-contrast-fg: #ffffff;
  --zui-scroll-expand-contrast-hint: #0f172a;
  --zui-scroll-expand-tint-scrim: linear-gradient(to top,#312e81e6,#4338ca33 45%,#1e1b4b80);
  --zui-scroll-expand-tint-fg: #eef2ff;
  --zui-scroll-expand-tint-hint: #4338ca;
  --zui-scroll-expand-title-weight: 700;
  --zui-scroll-expand-title-tracking: -0.03em;
  --zui-scroll-expand-title-size: clamp(1.75rem,7.5cqi,5.25rem);
  --zui-scroll-expand-title-shadow: 0 2px 24px #00000073;
}

/* Dark theme variables follow the same names with -dark appended. */
.dark {
  --zui-scroll-expand-radius-dark: 24px;
  --zui-scroll-expand-bg-dark: transparent;
  /* ...same variables with -dark at the end */
}

Overview

Import ScrollExpand from the scroll-expand entry and pass an image or video src. title renders as a real heading (level 2 by default) that labels the section, hint is a small aria-hidden cue, and children are the overlay revealed at full bleed. The defaults are scroller window, startWidth 42, startHeight 58, zoom 1.35, distance 1.2, hold 0.35, smoothing 100 ms, and appearance default.

With scroller window, the section follows the page or any scrolling ancestor, and its stage is one small-viewport height tall. Pinning uses position: sticky, so no ancestor between the section and its scroller may set overflow hidden, auto, or scroll; use overflow-clip to round or clip a wrapper. With scroller self, the component is its own focusable scroll container; give it a height with className. The current phase is exposed as data-state (resting, expanding, expanded) for styling.

Consumers using Tailwind v4 must include the package in their @source scan so both light and dark token classes are generated.

Common use cases

  • Product and campaign heroes that open onto a photo or film.
  • Chapter breaks in long-form and editorial pages.
  • Portfolio and case-study showcases inside a fixed-height frame.

FAQ

Does it hijack scrolling?

No. It never intercepts wheel or touch input. A tall track and a sticky stage pin the section while the page, or the component itself, scrolls normally.

Can keyboard users reach the overlay content?

Yes, once it is visible. Overlay content stays inert until it is mostly revealed, so focus never lands on invisible links. A self-scrolling section is focusable and scrolls with the arrow and Page Down keys.

What happens with reduced motion?

The section shows its expanded composition at its normal height, without pinning or scroll-linked movement, and video never plays.