Browse the docs
HooksNo. 105 of 109
useEntrance
When a one-shot entrance runs: on first paint, or once in view, without hiding server-rendered content that is already on screen.
Installation
pnpm dlx shadcn@latest add @corsair-ui/use-entranceNo setup needed: the CLI finds @corsair-ui in the shadcn registry directory. The files land in your project as source: edit them like your own code. More in Installation.
- Also adds
- use-in-view
API
| Prop | Type | Default |
|---|---|---|
triggerPlay on first paint, from CSS alone, or once the element scrolls into view. | "load" | "in-view" | "in-view" |
onceIn view: play the first time only; otherwise rearm off screen. | boolean | true |
amountIn view: fraction of the element that has to be visible. | number | 0.3 |
playTakes over from trigger: false holds the start state, true plays. | boolean | — |
returnsphase is "static", "armed" or "play"; render the start state for armed. | [ref, phase, inView] | — |
Accessibility
- Server-rendered content already on screen is never hidden while the page hydrates.
- Components render their start state and animation behind motion-safe, so reduced motion shows the content as is.
Compatibility
- Tailwind CSS
- 3.4 and 4, both checked in CI
- React
- 19
- Rendering
- Works in server components
Source
1 file, installed as source you own
use-entrance.tsView on GitHubimport { useCallback, useRef, useState, useSyncExternalStore } from "react";
import { useInView } from "@/registry/default/hooks/use-in-view";
export type EntranceTrigger = "load" | "in-view";
/**
* - `static`: show the content as it is, with no animation.
* - `armed`: hold the content in its starting state, waiting to play.
* - `play`: run the entrance.
*/
export type EntrancePhase = "static" | "armed" | "play";
export interface UseEntranceOptions {
/**
* "load" plays on first paint, from CSS alone, so it suits text above the
* fold and needs no JavaScript. "in-view" waits until the element scrolls
* into view.
*/
trigger?: EntranceTrigger;
/** In-view: play the first time only. Otherwise it rearms off screen and plays again. */
once?: boolean;
/** In-view: fraction of the element that has to be visible, from 0 to 1. */
amount?: number;
/** Takes over from `trigger`: `false` holds the content in its starting state, `true` plays it. */
play?: boolean;
}
const subscribe = () => () => {};
/**
* Decides when a one-shot entrance runs. Components render their start
* state for `armed` and their animation for `play`, both behind
* `motion-safe`, so with `prefers-reduced-motion` the content simply shows.
*
* Server-rendered content is never hidden before the browser has checked
* where it is: if it is already on screen it stays `static` (use "load" for
* that), so nothing blinks while the page hydrates. Elements first rendered
* in the browser (a dialog, a tab, a remount) start `armed` and play as
* soon as they are seen.
*
* @example
* const [ref, phase] = useEntrance<HTMLSpanElement>({ trigger: "in-view" });
* return <span ref={ref} data-state={phase}>…</span>;
*/
export function useEntrance<T extends Element = Element>({
trigger = "in-view",
once = true,
amount = 0.3,
play,
}: UseEntranceOptions = {}) {
// False while hydrating server HTML, true for elements first rendered in the browser.
const browserRender = useSyncExternalStore(
subscribe,
() => true,
() => false
);
const [phase, setPhase] = useState<EntrancePhase>(() => {
if (trigger === "load") return "play";
return browserRender ? "armed" : "static";
});
const answered = useRef(false);
const watching = trigger === "in-view" && play === undefined;
const onChange = useCallback(
(inView: boolean) => {
const first = !answered.current;
answered.current = true;
setPhase((current) => {
if (inView) return current === "armed" ? "play" : current;
// Rendered on the server but below the fold: hide it until it is reached.
if (current === "static" && first) return "armed";
if (current === "play" && !once) return "armed";
return current;
});
},
[once]
);
const [observe, inView] = useInView<T>({ amount, onChange: watching ? onChange : undefined });
const current: EntrancePhase = play === undefined ? phase : play ? "play" : "armed";
return [observe, current, inView] as const;
}