Skip to main content

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 same with from 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 sets in or after.

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:

  • behavior sets its behavior, e.g., scoped for callouts. It takes precedence over the default of the slide and the theme.
  • split and overlay set how it asks for pages when the slide is split into pages (see Splitting into pages), e.g., overlay for 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, or past,
  • isActive and isCurrent,
  • appearance and className: 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 its duration and easing, like transition: 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 shown of useStepTransition() is true, since the views may still show the old step for a moment, and
    • jump to their end once skipped is true, since the presenter has moved on.
  • Keep animations short. Presenters step quickly.
  • Respect reduced motion. useStepTransition already does. For other animations, like ones that loop, check useSlideView().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.