For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Reveal

An entrance animation for list items, cards and page sections. The element fades in and rises 12px.

Installation

$ pnpm dlx shadcn@latest add @cobalt/reveal

The reveal keyframe is installed with the component, so it works in any project.

Usage

{
  items.map((item, index) => (
    <Reveal key={item.id} index={index} render={<Card />}>
      ...
    </Reveal>
  ));
}
PropDefaultUse
index0Position in a list. Each step waits step ms longer.
step40Length of one stagger step in ms.
delay0Extra wait in ms, added to the stagger.
blurfalseAlso fades in from a 3px blur, for page sections.
renderdivThe element to render, for example <Card /> or <li />.

Cap long lists yourself, for example index={Math.min(index, 8)}.

Motion

The element rises 12px and fades in over 500ms with cubic-bezier(0.22, 1, 0.36, 1). It keeps its end state afterwards. With prefers-reduced-motion: reduce the animation is removed and the element is shown in place, so nothing is hidden.

Source

"use client";

import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cn } from "cn";
import * as React from "react";

/** True inside a `Reveal`, so a nested one does not play a second entrance. */
const RevealContext = React.createContext(false);

type RevealProps = useRender.ComponentProps<"div"> & {
  /**
   * `card` is the entrance of a whole card: 300ms, from a 6px blur and 4px
   * lower.
   */
  variant?: "default" | "card";
  /** Position in a list. Each step waits `step` ms longer than the one before. */
  index?: number;
  /** Extra wait in ms, added to the stagger. */
  delay?: number;
  /** Length of one stagger step in ms. */
  step?: number;
  /** Also fades in from a 3px blur. */
  blur?: boolean;
};

/**
 * Entrance animation: the element fades in and rises 12px over 500ms.
 *
 * It is a plain CSS animation with `both` fill, so the server HTML already
 * paints the first (hidden) frame and nothing waits for hydration: the content
 * is never shown and then hidden. Without JavaScript it still plays, and
 * reduced motion removes it, so the element stays readable.
 *
 * A `Reveal` inside another `Reveal` does not animate: the outer one already
 * brings it in, and two stacked entrances read as a flicker.
 */
function Reveal({
  className,
  variant = "default",
  index = 0,
  delay = 0,
  step = 40,
  blur = false,
  render,
  ...props
}: RevealProps) {
  const nested = React.useContext(RevealContext);
  const card = variant === "card";
  const wait = Math.max(0, index * step + delay);
  const element = useRender({
    defaultTagName: "div",
    props: mergeProps<"div">(
      nested
        ? { className }
        : {
            className: cn(
              "animate-[reveal_var(--reveal-duration,500ms)_cubic-bezier(0.22,1,0.36,1)_var(--reveal-delay)_both] motion-reduce:animate-none!",
              className
            ),
            style: {
              "--reveal-delay": `${wait}ms`,
              "--reveal-filter":
                card || blur ? `blur(${card ? 6 : 3}px)` : "none",
              ...(card && {
                "--reveal-duration": "300ms",
                "--reveal-rise": "4px",
              }),
            } as React.CSSProperties,
          },
      props
    ),
    render,
    state: { slot: "reveal" },
  });
  return (
    <RevealContext.Provider value={true}>{element}</RevealContext.Provider>
  );
}

export { Reveal };
export type { RevealProps };