Reveal
Free · MITHeadless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.
Install
Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).
Unstyled — behavior only, you write the CSS
npx astrocraft-ui add media/revealPlain-CSS theme — no build step
npx astrocraft-ui add media/reveal --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add media/reveal --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add media/reveal --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add media/reveal --theme tailwind --bridge lumosLive demo
Reveal
Scroll-driven, zero-JS — sixteen entrances, every one ending at the element's resting state, so content that never scrolls into view is still readable. animate= opts a call site out entirely — the last box.
Attribute surface
With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.
| Component | Slots | Variants | Runtime state | Native state |
|---|---|---|---|---|
| `Reveal.astro` | `reveal` | `data-animation`: `fade-in` · `fade-in-up` · `fade-in-down` · `fade-in-left` · `fade-in-right` · `zoom-in` · `blur-in` · `bounce-in` · `flip-in-x` · `flip-in-y` · `rotate-in` · `roll-in` · `wipe-in-up` · `wipe-in-down` · `wipe-in-left` · `wipe-in-right` `data-range`: `entry` · `cover` · `contain` | — | — |
Source
What the command copies — 2 files, alias-free so the copy lands standing alone. This is the whole component; there is nothing else.
---
// src/components/ui/media/reveal/Reveal.astro — headless primitive (see ../../README.md).
// Scroll-reveal wrapper. It ships the ELEMENT and the intent (`data-animation`, `data-range`); the
// keyframes and the scroll timeline are the theme's. The reference theme drives it with the NATIVE
// scroll timeline (`animation-timeline: view()`), so it stays zero-JS.
//
// Two rules a theme MUST honor. CSS cannot enforce them — but a script can, and one does:
// `pnpm headless` runs `scripts/motion-contract.mjs` over every stylesheet in the repo, and a
// consumer can point it at their own theme the same way. Both failures end as content that is
// permanently invisible, which is why they are a gate and not a comment.
// • `prefers-reduced-motion` — a scroll-driven animation is progressed by scroll position, not
// time, so the usual "zero the duration" reset does NOT stop it. The theme has to remove the
// animation outright (`animation: none`) under the reduced-motion query.
// • The element must end at its resting state, or content that never enters the viewport (and
// every browser without scroll timelines) stays stuck mid-animation.
//
// `animate={false}` renders a plain pass-through wrapper with no animation attributes — that is the
// per-call escape hatch. In the Tailwind original this also followed a `siteSettings.useAnimations`
// build flag; a standalone library has no site config to read, so the prop is the whole switch and a
// consumer gates it at their own call sites.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"div"> & {
/** Render element (the wrapper needs a box — `display: contents` would break the timeline). */
as?: keyof HTMLElementTagNameMap;
/** Set false to opt this call site out of motion entirely. */
animate?: boolean;
/**
* Which entrance the theme plays. ENTRANCES ONLY, and that is not squeamishness: a scroll-driven
* animation ends wherever its keyframe ends, so an "out" animation would leave the content gone
* for good. `pnpm headless` fails the build if a theme defines one of these ending anywhere but
* the element's resting state.
*/
animation?:
| "fade-in"
| "fade-in-up"
| "fade-in-down"
| "fade-in-left"
| "fade-in-right"
| "zoom-in"
| "blur-in"
| "bounce-in"
| "flip-in-x"
| "flip-in-y"
| "rotate-in"
| "roll-in"
| "wipe-in-up"
| "wipe-in-down"
| "wipe-in-left"
| "wipe-in-right";
/** Window of the element's view-progress the animation plays over. */
range?: "entry" | "cover" | "contain";
};
const {
animation = "fade-in-up",
range = "entry",
as,
animate = true,
class: className,
...rest
} = Astro.props;
const Tag = as ?? "div";
---
<Tag
class={className}
data-slot="reveal"
data-animation={animate ? animation : undefined}
data-range={animate ? range : undefined}
{...rest}><slot /></Tag
>
import Reveal from "./Reveal.astro";
export { Reveal };
export default Reveal;
What you get
The component source, copied into your project by npx astrocraft-ui add media/reveal — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.