Motion

Reveal

A headline line rising out of its own baseline.

betav0.1.0 Chromium 103 ui-scale safe

Install

Copies the file into components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.
bunx shadcn@3 add @fastrp/reveal

Examples

Default

Open alone

Şehir uyanıyor.
Sen hazır mısın?

API

<Reveal />

A line that rises out of its own baseline — the headline entrance. The clip is the wrapper's own box, so the travel is a percentage of the line and needs no pixel length. Under reduced motion it fades in where it stands. The wrapper is what is watched, not the line: the line starts fully clipped by the wrapper's `overflow: hidden`, and an IntersectionObserver counts a clipped element as never visible.

PropTypeDefaultNotes
as"div" | "span"divThe element the line is set in.
classNamestring—
delaynumber0Seconds.

Dependencies

npm

  • @fastrp/design-tokens
  • motion@^12.40.0

Registry

Source

components/ds/reveal.tsxShow
'use client';

import { DURATION_S, EASE } from '@fastrp/design-tokens/motion';
import { motion, useInView, useReducedMotion } from 'motion/react';
import type * as React from 'react';
import { useRef } from 'react';
import { useScrollRoot } from '@/components/ds/scroll-root';
import { cn } from '@/lib/utils';

// @fast-ds [email protected]

export type RevealProps = {
  children: React.ReactNode;
  /** Seconds. */
  delay?: number;
  className?: string;
  /** The element the line is set in. */
  as?: 'div' | 'span';
};

/**
 * A line that rises out of its own baseline — the headline entrance. The clip is the wrapper's
 * own box, so the travel is a percentage of the line and needs no pixel length. Under reduced
 * motion it fades in where it stands.
 *
 * The wrapper is what is watched, not the line: the line starts fully clipped by the wrapper's
 * `overflow: hidden`, and an IntersectionObserver counts a clipped element as never visible.
 */
function Reveal({ children, delay = 0, className, as = 'div' }: RevealProps) {
  const reduceMotion = useReducedMotion();
  const root = useScrollRoot();
  const ref = useRef<HTMLElement>(null);
  const inView = useInView(ref, { once: true, root });
  const Wrapper = as as 'div';
  const Inner = as === 'span' ? motion.span : motion.div;

  return (
    <Wrapper
      ref={ref as React.RefObject<HTMLDivElement>}
      data-slot="reveal"
      className={cn('block overflow-hidden', className)}
    >
      <Inner
        className="block"
        initial={{ y: reduceMotion ? '0%' : '105%', opacity: reduceMotion ? 0 : 1 }}
        animate={inView ? { y: '0%', opacity: 1 } : undefined}
        transition={{ duration: DURATION_S.slower, delay, ease: EASE }}
      >
        {children}
      </Inner>
    </Wrapper>
  );
}

export { Reveal };