Steps in components
Wrapping content in a step
Step renders an element that is shown according to the steps of the slide.
It takes the props of :step, like with, group, in, after, title,
and behaviors, and passes the others to its element:
import { Step } from '@seminar/core';
<Step component="section" only>
Shown only during its step
</Step>;
The position of a step in the document decides its order and nesting: steps inside a step are nested in it.
Components that should take part in steps wrap their root in a Step, and
pass on the step props they get, e.g., from :::name{step group=parse}:
import { Step, JOINS_CONTENT, type StepProps } from '@seminar/core';
interface NoteProps extends Omit<StepProps, 'defaults'> {
tone?: string | undefined;
}
export function Note({ tone, ...stepProps }: NoteProps) {
return <Step defaults={JOINS_CONTENT} data-tone={tone} {...stepProps} />;
}
Default placement
Without placement props, a Step starts a new step. Components choose what they
do by default with defaults:
JOINS_CONTENT({ joins: 'content' }) never creates a step, like paragraphs, list items, images, and columns: the component is part of the latest step on its level that isn't an overlay, and looks like it, and steps inside it are nested in that step. If there is no such step, it's simply part of its parent, or of the slide.with,step,in,after, a behavior, or a group with other elements give it a step instead.{ with: 'content' }works like the samewithfrom the user: the component is shown with that step, and is part of it unless it has a behavior of its own. If there is no such step, it creates one, shown up front.{ in: element }or{ in: 'name' }nests it in the step of an element or of a group, like callouts pointing at an element, unless the user setsinorafter.
joins and with also take 'previous', the step right before it, and
'initial', the step shown as soon as its parent is.
Defaults other than joins can also set what the component does as a step, unless
the user says otherwise:
behaviorsets its behavior, e.g.,scopedfor callouts. It takes precedence over the default of the slide and the theme.splitandoverlayset how it asks for pages when the slide is split into pages (see Splitting into pages), e.g.,overlayfor callouts, which float over other content.
Prefer these defaults over setting with or behavior directly, so that
users can still override them with attributes.
Reacting to steps
useStep() and useSteps(count) create a stepper without rendering an
element. Attach the returned ref to the elements that place it in the
document, and read its state:
stepIndex: the current step of the stepper, from 0, or -1 before it,stepCount: the number of steps,phase: where the presentation is relative to the stepper, e.g.,future,current, orpast,isActiveandisCurrent,appearanceandclassName: how the elements should appear, e.g., hidden or dimmed, and the class that makes them.
import { useSteps } from '@seminar/core';
export function Highlights({ parts }: { parts: string[] }) {
const { ref, stepIndex } = useSteps(parts.length, { behavior: 'always' });
return (
<ul ref={ref}>
{parts.map((part, index) => (
<li key={part} style={{ fontWeight: index === stepIndex ? 700 : 400 }}>
{part}
</li>
))}
</ul>
);
}
The behavior always keeps everything visible here, and only the highlight
follows the steps. <Steps count={n}> does the same with a render function as
its children.
useStep and useSteps take the props of Step, so a component that takes
StepProps can pass them on as they are, and places and appears like a Step:
import { StepAppearanceContext, useStep, type StepProps } from '@seminar/core';
export function Note({ children, ...stepProps }: StepProps) {
const { ref, className, appearance } = useStep(stepProps);
return (
<div ref={ref} className={className}>
<StepAppearanceContext value={appearance}>
{typeof children === 'function' ? null : children}
</StepAppearanceContext>
</div>
);
}
Every slide styles className, so components don't need any CSS of their own, and
it also works on elements that can't be a Step, like the parts of a figure. The
context keeps steps inside the element from applying the same appearance again.
Without a behavior, the hooks take the default of the slide, so steps nested in
them wait for them, like in a Step. Pass always for content that stays shown.
Splitting a slide into pages goes by the behavior, too, so a component whose
content changes in its steps otherwise should pass
defaults: { split: 'each' }, like <Steps>, to get a page of each step. A handle
returned by useStep can also be passed to the with of other steps, which then
share its stepper, e.g., to reveal items of two lists in parallel.
Animations
The slide fades from one step to the next by itself (see Themes). So content that appears, disappears, or dims with the steps needs no animation of its own.
If a component animates in other ways, like moving a marker, follow these rules:
- Don't fade steps yourself. A step that also fades with CSS fades twice, and the steps nested in it show through.
- Render the current step, and let CSS animate. Compute how the current
step looks, like where the marker is for
stepIndex, and animate changes with a CSS transition. Don't start animations in effects: the presenter can jump to any step, and then the component must show the end state at once. - Take the timing from
useStepTransition(). Use itsdurationandeasing, liketransition: transform ${duration}ms ${easing}. The duration is 0 whenever the step should change at once, so you don't have to check for that yourself. - Ask for the time your animation needs. Set the fade of your step to
the shortest time in which the animation still explains what changes,
e.g.,
useStep({ fade: 500 }). Steps that change at the same time can only make it longer, and the slide fades as long, too. - Expect a duration of 0 anyway. The author can cut the step, and the presenter can skip it, so the component must also work without its animation. Don't cut steps yourself to make them fast: leave out the fade to take the time of the theme, or ask for 0, which other steps can outweigh.
- Keep animations in JavaScript in step. CSS transitions and animations
do this by themselves. Animations in JavaScript, like drawing on a canvas,
should:
- start only once
shownofuseStepTransition()istrue, since the views may still show the old step for a moment, and - jump to their end once
skippedistrue, since the presenter has moved on.
- start only once
- Keep animations short. Presenters step quickly.
- Respect reduced motion.
useStepTransitionalready does. For other animations, like ones that loop, checkuseSlideView().animated. - Show the same in every view. The audience and presenter views render the slide separately. Base animations only on the steps and the slide state, not on random values or when the component mounted.
In Firefox and Safari, the slide cuts instead of fading. useStepTransition
still gives the timing of the step there, so components animate as usual.
Animating outside of the fade
While the slide fades, a moving element fades too, which looks like a blurry
cross-fade between where it was and where it goes. To keep an element out of
the fade between steps, add useOwnAnimation() to its style. The element
then shows its new state at once, and its own transition plays in full. It
still fades when the slide changes.
import { useOwnAnimation, useStep, useStepTransition } from '@seminar/core';
export function Marker() {
const step = useStep({ group: 'parse' });
const ownAnimation = useOwnAnimation();
const { duration, easing } = useStepTransition();
return (
<div ref={step.ref}>
<div
style={{
...ownAnimation,
transform: step.isActive ? 'translateX(200px)' : 'none',
transition: `transform ${duration}ms ${easing}`,
}}
/>
</div>
);
}
Such an element doesn't fade in or out with the steps. If it appears or disappears, animate that yourself, too.
The Pipeline component of the example deck shows all of this. Its stages
light up with the list items, and fade with the slide. Its marker moves from
stage to stage by itself, as slowly as the steps fade. Before the last
stage, it holds the change with useTransitionHold to pause for a moment.