Editorial Reveal

A thin reveal primitive for editorial prose, callouts, and explainer beats. The wrapper owns no state of its own — the parent picks the current trigger and renders the matching children. When trigger flips, the previous body exits and the new body enters under the chosen variant (fade, lift, or blur).

Reach for it when a single slot of prose needs to morph in place between named beats — a hint that updates, an aside that swaps tone, an insight that lands after a struggle.

Now look again. Every comparison after the first one repeats work we already did. The structure has memory we are throwing away.
Customize
Beat
insight
Motion
lift

Installation

npx shadcn@latest add https://craftbits.dev/r/editorial-reveal.json

Usage

import { EditorialReveal } from "@craft-bits/core";
 
<EditorialReveal trigger={beat} variant="lift">
  {bodyForBeat(beat)}
</EditorialReveal>

Use fade for the quietest swap — opacity only, no displacement:

<EditorialReveal trigger={beat} variant="fade">
  {bodyForBeat(beat)}
</EditorialReveal>

Use blur for a set-piece reveal where the content should feel like it is coming into focus:

<EditorialReveal trigger={beat} variant="blur">
  {bodyForBeat(beat)}
</EditorialReveal>

Understanding the component

  1. Keyed reveal. The body is wrapped in <AnimatePresence mode="popLayout" initial={false}> and keyed by trigger. When trigger changes, the previous body exits and the new body enters under the variant's motion shape.
  2. initial={false}. The first render does not animate; only subsequent trigger changes do. Skips the first-render fade so the page does not flicker on load.
  3. mode="popLayout". Old and new bodies coexist mid-transition without pushing siblings around, so a tall body cross-fading with a short one does not cause the page to twitch.
  4. Variant motion. fade is opacity only on SPRINGS.damped. lift adds a 6px upward translate on SPRINGS.smooth. blur adds a 6px filter blur on SPRINGS.damped. Every variant is transform / opacity / filter only — never width / height / top / left.
  5. Reduced motion. When prefers-reduced-motion: reduce is set, every variant collapses to an instant opacity cross-fade.
  6. Aria-live polite. The body region carries aria-live="polite" so screen readers re-announce the new content when trigger advances, without interrupting the user.

Props

PropTypeDefaultDescription
triggerstring | numberrequiredStable identifier for the current revealed body. Drives the AnimatePresence key.
variant'fade' | 'lift' | 'blur''lift'Animation shape for the reveal.
childrenReactNoderequiredBody to reveal for the current trigger.
classNamestringMerged onto the root via cn().

Accessibility

  • The body wrapper is role="group" with aria-live="polite" so screen readers announce the new content on swap without interrupting.
  • Animation is transform / opacity / filter only — never width / height / top / left.
  • Exiting bodies receive pointer-events: none so a mid-transition pointer cannot interact with content that is on its way out.
  • prefers-reduced-motion: reduce short-circuits every variant to an instant opacity cross-fade — no displacement, no blur, no spring.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/chrome/EditorialReveal.tsx). The source rendered a New Yorker / nan.fyi-styled em-dash lead-in with semantic-color variants baked into a single static paragraph. craft-bits' version drops the typographic lead-in (parent owns its own copy) and exposes a generic three-prop reveal API (trigger, variant, children) so the same primitive composes with any editorial flow.