✨ Animation
How do I reveal a list one line at a time, or move between slides with something other than a cut?
Fragments
A fragment reveals part of a slide on the next keypress instead of showing everything at once.
import { Fragment } from "@getnarro/shared-ui";
<Slide id="reveal">
<SlideContent>
<Heading level={2}>Progressive reveal</Heading>
<Fragment order={1}>
<Text>Appears first</Text>
</Fragment>
<Fragment order={2} effect="slide-up">
<Text>Appears second, sliding up</Text>
</Fragment>
<Fragment order={3} effect="scale-in" duration={400}>
<Text>Then this</Text>
</Fragment>
</SlideContent>
</Slide>;
Fragment comes from @getnarro/shared-ui, not @getnarro/core.
| Prop | Type | Default |
|---|---|---|
order | number | source order |
effect | FragmentEffect | "fade-in" |
duration | number (ms) | — |
staggerDelay | number (ms) | — |
isActive | boolean | inherited from the slide |
FragmentEffect is “fade-in” | “fade-out” | “slide-up” | “slide-down” | “slide-left” | “slide-right” | “scale-in” | “scale-out” | “blur-in”.
Fragments consume a navigation step: on a slide with three fragments, the first
three presses reveal them and the fourth advances. Every key that advances a
slide advances a fragment too, including the PageDown a presenter’s clicker
sends.
Without an explicit order, fragments reveal in source order. Set order when
you want a different one — two fragments sharing an order reveal together.
trigger="time" opts out: those fragments reveal on slide entry, staggered by
staggerDelay, and do not consume a press — a slide holding only time-triggered
fragments advances on the first press, not on the nth.
Fragments without an explicit order are numbered in source order, and the
numbering depends only on what is mounted — so a slide costs the same number of
presses every time it is entered.
<Fragment>revealed first</Fragment>
<Fragment>then this</Fragment>
<Fragment trigger="time">already there when the slide opens</Fragment>
Used outside a Slide — a component in a storybook, a page that is not a deck
— there is nothing to step, so a fragment reveals on mount.
In markdown mode the equivalent is {.step}, and it behaves the same way:
- appears first {.step}
- appears second {.step delay=200}
Slide transitions
<Slide transition="fade">…</Slide>
"none" | "fade" | "slide" | "zoom". In markdown, set transition: in slide
frontmatter, or on the deck to change the default for every slide.
Motion primitives
@getnarro/core wraps Framer Motion in components that already know whether
their slide is on screen — animations start when the slide appears, not when the
deck mounts.
import { Motion, MotionNumber, MotionText } from "@getnarro/core";
<Motion effect="slideUp" delay={0.2}>
<Heading level={1}>Animated in</Heading>
</Motion>
<MotionText type="wordByWord">One word at a time</MotionText>
<MotionNumber value={42} suffix="%" decimals={0} />
Units differ between the two families, and nothing warns. Durations and
delays on Motion* components are in seconds (Framer Motion’s convention);
everything in shared-ui — Fragment.duration, TypewriterText.speed,
CountingNumber.duration — is in milliseconds. duration={2} is two
seconds in one and two milliseconds in the other, and both are valid numbers.
The rule that always holds: core is seconds, shared-ui is milliseconds.
Which of the two to reach for
Fragment and MotionSteps/MotionStep both stage a slide in pieces, and they
are not interchangeable — the difference is who owns the presenter’s keypresses:
Fragment (shared-ui) | MotionSteps / MotionStep (core) | |
|---|---|---|
| Navigation | Consumes a keypress per fragment. The slide takes N presses to leave | Consumes nothing. It manages its own index and needs totalSteps |
| Units | Milliseconds | Seconds |
| Built on | CSS keyframes | Framer Motion |
| Reach for it when | The presenter should reveal the slide at their own pace — the common case | The sequence is a self-contained animation you want to replay or drive yourself |
If you want “press space, the next thing appears”, that is Fragment.
Mixing both on one slide means two things claiming the same keypress, so pick
one per slide.
MotionEffect is “fadeIn” | “fadeOut” | “slideUp” | “slideDown” | “slideLeft” | “slideRight” | “scaleUp” | “scaleDown” | “rotateIn” | “flip” | “bounce” | “elastic” | “blur” | “glow” | “typewriter” | “spotlight” | “parallax” | “morphPath” | “stagger” | “wave”.
See the component reference for the full set — Motion,
MotionContainer, MotionList, MotionText, MotionNumber,
MotionPresence, MotionSpotlight, MotionSteps, MotionStep,
MotionTransform.
Gating your own animation
To drive animation yourself, ask whether the slide is active. Both packages
export the hook — @getnarro/shared-ui and @getnarro/core — and both read the
context Slide provides, so either import works:
import { useSlideActive } from "@getnarro/shared-ui";
function Chart() {
const isActive = useSlideActive();
return <svg className={isActive ? "animate-draw" : "opacity-0"} />;
}
Without this, every animation on every slide fires on load and is finished before the audience sees it.
Respecting reduced motion
Motion components honour prefers-reduced-motion. If you hand-roll animation,
gate it the same way:
@media (prefers-reduced-motion: reduce) {
.animate-draw {
animation: none;
}
}