Detent web components
By Shane Logsdon

Media & effects

Scroll Velocity Text

Implemented

Direction-aware kinetic text that expands and contracts with scroll

detent-scroll-velocity-text

Port status
Catalog status
implemented
Origin
upstream
Phase
4
Wave
phase-4-cinematic
Group
Media & effects

3.8 KB gzip for the class module

Variants

Scroll this page down and up: the text in the main preview expands and fades out with downward scroll, winds the same progress back up, and holds where it is when scrolling stops. It is one continuous text node, never per-letter spans.

A quieter configuration with a later fade (fade-start="0.7") and a smaller scale ceiling. Both use the documented defaults for smoothing-ms (400) and transform-origin (center center).

prefers-reduced-motion: reduce locks every instance to its resting frame: no scroll tracking, no frame, no transform. An empty text is the documented disabled state and behaves the same way.

Recorded states

  • disabled recorded state; no setter
  • expanded recorded state; no setter
  • reduced motion recorded state; no setter

Usage

Import

import '/components/scroll-velocity-text/define.js';

// or register it yourself
import { DetentScrollVelocityText } from '/components/scroll-velocity-text/scroll-velocity-text.js';

Attributes

  • expand-distance
  • fade-start
  • intensity
  • max-letter-spacing
  • max-scale
  • min-letter-spacing
  • smoothing-ms
  • text
  • transform-origin

Properties

  • as
  • scrollContainerRef

Slots

No slots are recorded for this component.

Events

No events are recorded for this component.

Example

Runnable example: site/examples/scroll-velocity-text.ts. The snippet below is generated from the catalogued attributes.

<detent-scroll-velocity-text
  expand-distance="500"
  fade-start="0.4"
  intensity="1"
  max-letter-spacing="1"
  max-scale="1.6"
  min-letter-spacing="0"
  smoothing-ms="400"
  text
  transform-origin="center center"
></detent-scroll-velocity-text>

Property-only values

const element = document.querySelector('detent-scroll-velocity-text');
if (element !== null) {
  // as: keyof JSX.IntrinsicElements
  element.as = 'span';
  // scrollContainerRef: RefObject<HTMLElement | null>
  element.scrollContainerRef = undefined;
}

Source

The pinned upstream source for this row:

  • src/components/ui/Scrollvelocitytext.tsx sha256 e948f3c06de6…

Native files

The native module and its artifacts:

  • src/components/scroll-velocity-text/scroll-velocity-text.ts
  • src/components/scroll-velocity-text/styles.css
  • src/components/scroll-velocity-text/define.ts
  • docs/components/scroll-velocity-text.md
  • site/examples/scroll-velocity-text.ts
  • tests/browser/scroll-velocity-text.spec.ts
  • evidence/components/scroll-velocity-text.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
expand-distanceexpandDistancenumber500Kebab-case attribute; camelCase property.
fade-startfadeStartnumber0.4Kebab-case attribute; camelCase property.
intensityintensitynumber1Kebab-case attribute; camelCase property.
max-letter-spacingmaxLetterSpacingnumber1Kebab-case attribute; camelCase property.
max-scalemaxScalenumber1.6Kebab-case attribute; camelCase property.
min-letter-spacingminLetterSpacingnumber0Kebab-case attribute; camelCase property.
smoothing-mssmoothingMsnumber400Kebab-case attribute; camelCase property.
texttextstring—Kebab-case attribute; camelCase property.
transform-origintransformOriginstring'center center'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
askeyof JSX.IntrinsicElements'span'Property-only: assign from script; not accepted as an attribute.
scrollContainerRefRefObject<HTMLElement | null>—Property-only: assign from script; not accepted as an attribute.

Events

No custom events are recorded for this component.

Slots

No slots are recorded for this component.

CSS customisation

  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • as accepts the documented allow-list instead of any intrinsic element; an unknown value falls back to span with one warning.
  • scrollContainerRef accepts an element as well as the upstream React ref shape.
  • The upstream alias export Scrollvelocitytext is dropped; the pure helpers are the only additional named exports.
  • The pinned motion is an exponential damping loop, not a spring: the shared spring helper is deliberately not used because a spring would change the recorded velocity-to-offset mapping, so no PC-10.4 approval is pending.
  • The disabled state is nothing to track: empty text pins the rest frame and schedules no frame.
  • className and Tailwind typography become the three custom properties plus the text part.

What it does

Kinetic text that expands its letter spacing, scales up and fades out while the page (or a container) scrolls down, winds the same progress back up, and holds position when scrolling stops. The text stays one continuous native text node.

Upstream behaviour
  • Direction-aware scroll physics: expands on downward scroll, winds back on upward scroll, and holds position on idle
  • Zero per-frame React re-renders: progress is written directly to the DOM via requestAnimationFrame batching
  • GPU-accelerated CSS transitions with cubic-bezier easing for buttery smooth kinetic tracking
  • Screen-reader safe: rendered as an unbroken native text node rather than fragmented per-letter DOM spans
  • Customizable expansion range, letter spacing bounds (em), scale multiplier, and fade threshold
  • Flexible polymorphic tag rendering via the "as" prop (h1, h2, span, p, etc.) with custom transform-origin
  • Supports both global window scroll and scoped scrollable container refs
  • Built-in prefers-reduced-motion detection automatically locks text to resting state
Differences from upstream
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • as accepts the documented allow-list instead of any intrinsic element; an unknown value falls back to span with one warning.
  • scrollContainerRef accepts an element as well as the upstream React ref shape.
  • The upstream alias export Scrollvelocitytext is dropped; the pure helpers are the only additional named exports.
  • The pinned motion is an exponential damping loop, not a spring: the shared spring helper is deliberately not used because a spring would change the recorded velocity-to-offset mapping, so no PC-10.4 approval is pending.
  • The disabled state is nothing to track: empty text pins the rest frame and schedules no frame.
  • className and Tailwind typography become the three custom properties plus the text part.
Previous: Scroll Progress Navigation Next: Bulk Action Bar

Docs

Components

Project