Marp CLI: How to make custom transition

Marp CLI v2 has supported brand-new page transitions for the bespoke HTML template. You can use this stable transition support in either Marp CLI v2.4.0+ or Marp for VS Code v2.5.0+.

Effective transitions will help make a dramatic presentation. Adding a touch of effects to slides is often common in great talks. By viewing HTML slide in the browser that supports View Transitions API (Chrome 110+), or Marp CLI with --preview option, you can start to use varied 33 transition effects out of the box, by just a simple definition transition directive.

Built-in transitions should be useful for 90% of Marp users. But what you can do if there are no effects you are satisfied with? Make your effects in CSS! Marp can register your custom animation set declared in CSS as a named transition, and use it in the Markdown slide.


This article will describe the following things:

  1. The anatomy of a transition: How the transition effect will work in Marp
  2. Declare custom transitions: How to register custom transitions by CSS
  3. Helpful tips for making your transition

See also the official documentation about transitions in Marp CLI.

If using built-in transitions made by us was enough, you don't need to read this article. Please save your time, with keeping enjoying our transitions in your Markdown slide! :)

In this article, the word "transition" is meaning the slide transition effect in Marp. Please note that it is not meaning transition property in CSS.

The anatomy of a transition

The first what the custom transition author has to know is "How the page transition effect is realized in a presentation slide".

Let's consider what is happening when the slide page was navigated from 1 to 2. If no transitions were set to the slide, the first page will just disappear, and appear on the second page immediately. If it has a transition effect, a certain time for playing animations will insert between switching pages.

The anatomy of a transition
The anatomy of a transition

An important thing during transition is that 2 slides are presented in the view at the same time like layers. All kinds of effects produce smooth transitions by applying specific animations to one or both slides.

In Marp, the slide page that was shown before transition calls as "Outgoing slide", and the next page to appear after transition calls as "Incoming slide". Slide pages may have an inverse relationship when brought the backward navigation, but the meaning of "incoming" and "outgoing" is always consistent.

If you could figure them out, you probably also grasp that you have to respect the following 2 principles:

  • The outgoing slide should have an animation to hide the slide.
  • The incoming slide should have an animation to show the slide.

If either or both was not respected in a transition effect, it would become a weird transition.

Marp CLI's bespoke template will make two slide layers when navigated, and apply suitable animation keyframes declared in CSS.

Declare custom transitions

Simple keyframe declaration

Let's get started with a simple keyframe declaration for the dissolve effect (also known as the cross-fade effect), to learn how to set custom transition animation. Marp uses standard syntax for CSS animation @keyframes to declare transitions.

When applying the dissolve effect to transition principles, you can derive that the effect needs these animations:

  • The outgoing slide has an animation to decrease opacity from 100% to 0%.
  • The incoming slide has an animation to increase opacity from 0% to 100%.

There are opposite changes with each other. In this case, you can define animations for both slide layers by one @keyframes declaration.

First, declare @keyframes at-rule with the conventional name specified by Marp in your Markdown.

  1. ---
  2. transition: dissolve
  3. style: |
  4. @keyframes marp-transition-dissolve {
  5. /* ... */
  6. }
  7. ---

  8. # Slide 1

  9. ---

  10. <!-- _class: invert -->

  11. # Slide 2

marp-transition-xxxxxxxx is the rule of animation name to register the transition with a simple declaration. For using declared transition in Marp slide, assign transition local directive with the name declared in xxxxxxxx.

This example is using style global directive to declare keyframes. Of course, you also can use the inline <style> element or custom theme CSS to declare.

Well, declare animation details at keyframes. In a simple declaration, you only have to set animation for the outgoing slide. For the incoming slide, Marp will set the animation in the reverse direction automatically.

  1. @keyframes marp-transition-dissolve {
  2. from {
  3. opacity: 1;
  4. }
  5. to {
  6. opacity: 0;
  7. }
  8. }

This example has been declared from keyframe for clarity, but you can omit it because opacity: 1 is a default style.

Did you want more? That's it! Try to test this transition in the HTML slide with the browser that supports View Transitions API, or a preview window in Marp CLI.

  1. npx @marp-team/marp-cli@^2.4.0 --preview ./

You have made the first custom transition!

In this article, the example is simplified for teaching how to make a custom transition, and there is a bit of difference from the built-in transition fade for getting the same effect. dissolve effect is looking good, but there is a general pitfall about cross fading.

Split animations into outgoing and incoming

A simple declaration should work in some transition types well, but it's not that all transitions have exactly contrary animations to each other. In reality, different animations for the outgoing slide and incoming slide are required in most cases.

For example, the slide up effect must have these animations:

  • The outgoing slide should move from the viewport to the upper outer.
  • The incoming slide should move from the lower outer to the viewport.

So you can declare split animations for each layer rather than declaring a single animation. Set @keyframes with the prefix of the target transition: marp-outgoing-transition-xxxxxxxx and marp-incoming-transition-xxxxxxxx.

  1. ---
  2. transition: slide-up
  3. style: |
  4. @keyframes marp-outgoing-transition-slide-up {
  5. from { transform: translateY(0%); }
  6. to { transform: translateY(-100%); }
  7. }
  8. @keyframes marp-incoming-transition-slide-up {
  9. from { transform: translateY(100%); }
  10. to { transform: translateY(0%); }
  11. }
  12. ---

  13. # Slide 1

  14. ---

  15. <!-- _class: invert -->

  16. # Slide 2

Unlike the simple transition, there is no auto-reversed animation in the incoming slide. Each animation should define in the right direction.

The timeline diagram of slide-up transition
The timeline diagram of slide-up transition

Transition for backward navigation

If you have tested the above slide-up transition example, you may have noticed that is having a move to up also when slide navigation going to back has occurred.

Wrong direction in slide up transition

It brings a wrong user interaction and is not intuitive. You should want to provide the animation for the correct direction when occurred backward navigation.

We are providing several solutions to deal with this.

--marp-transition-direction CSS variable

While playing transition, --marp-transition-direction CSS custom property (as known as CSS variables) will be available in @keyframes.

It provides 1 in forwarding navigation, or -1 in backward navigation. Using var(--marp-transition-direction) together with calc() function would be useful to calculate the position in response to the direction of slide navigation.

  1. @keyframes marp-outgoing-transition-slide-up {
  2. from { transform: translateY(0%); }
  3. to { transform: translateY(calc(var(--marp-transition-direction, 1) * -100%)); }
  4. }
  5. @keyframes marp-incoming-transition-slide-up {
  6. from { transform: translateY(calc(var(--marp-transition-direction, 1) * 100%)); }
  7. to { transform: translateY(0%); }
  8. }

And now, the slide-up custom transition is working completely in both directional navigation!

Slide up transition with correct directions

NOTE: Any other CSS variables defined in the context of animation keyframes cannot use in keyframes.

Set custom animations for backward transition

Alternatively, you also can set more animation keyframes that are specific for backward navigation.

Declare @keyframes with the backward- prefix to the custom transition name, just like as marp-transition-backward-xxxxxxxx. It is available in both simple keyframes declaration and split keyframes declaration.

  1. @keyframes marp-incoming-transition-triangle {
  2. /* Wipe effect from left top */
  3. from { clip-path: polygon(0% 0%, 0% 0%, 0% 0%); }
  4. to { clip-path: polygon(0% 0%, 200% 0%, 0% 200%); }
  5. }

  6. @keyframes marp-incoming-transition-backward-triangle {
  7. /* Wipe effect from right bottom */
  8. from { clip-path: polygon(100% 100%, 100% 100%, 100% 100%); }
  9. to { clip-path: polygon(-100% 100%, 100% -100%, 100% 100%); }
  10. }

In backward navigation, each layer will try to use the backward keyframes first, and fallback to the normal keyframes if not declared. To disable unintended fallback in backward animations, set an empty declaration of @keyframes.

  1. @keyframes marp-outgoing-transition-zoom-out {
  2. from { transform: scale(1); }
  3. to { transform: scale(0); }
  4. }
  5. @keyframes marp-incoming-transition-zoom-out {
  6. /* Send the incoming slide layer to back */
  7. from { z-index: -1; }
  8. to { z-index: -1; }
  9. }

  10. /* ⬇️ Declare empty keyframes to disable fallback ⬇️ */
  11. @keyframes marp-outgoing-transition-backward-zoom-out {}
  12. @keyframes marp-incoming-transition-backward-zoom-out {
  13. from { transform: scale(0); }
  14. to { transform: scale(1); }
  15. }

OK, I've described all about declarations for the custom transition!


Easing function

Each transition has a linear easing by default. You can specify animation-timing-function property within individual keyframes if you want.

Setting animation-timing-function: step-end; to a keyframe can make paused animation until the next keyframe.


We have a fixed duration time of 0.5s as default for every transition. If you want to set a different default duration for your custom transition, please set --marp-transition-duration property in the first keyframe (from or 0%).

  1. @keyframes marp-incoming-transition-gate {
  2. from {
  3. /* Set the default duration of the "gate" transition as 1 second. */
  4. --marp-transition-duration: 1s;

  5. clip-path: inset(0 50%);
  6. }
  7. to { clip-path: inset(0); }
  8. }

  9. @keyframes marp-outgoing-transition-backward-gate {
  10. from {
  11. /* You also can set a different default for backward transition as necessary. */
  12. /* --marp-transition-duration: 1.5s; */

  13. clip-path: inset(0);
  14. }
  15. to { clip-path: inset(0 50%); }
  16. }
  17. @keyframes marp-incoming-transition-backward-gate {
  18. from { z-index: -1; }
  19. to { z-index: -1; }
  20. }

The slide author can override the default duration at any time, through the transition local directive in Markdown (<!-- transition: fade 2s -->).

Fixed property

If some of the properties required a fixed value while playing transition, try to set the same declaration into from (0%) and to (100%).

  1. @keyframes marp-outgoing-transition-pin {
  2. /* Use fixed transform-origin */
  3. from {
  4. transform-origin: top left;
  5. animation-timing-function: ease-in;
  6. }
  7. to {
  8. transform-origin: top left;
  9. transform: rotate(90deg);
  10. }
  11. }

  12. @keyframes marp-incoming-transition-pin {
  13. /* Send the incoming slide layer to back */
  14. from { z-index: -1; }
  15. to { z-index: -1; }
  16. }

Layer order

As presented in a diagram earlier, the incoming slide layer always will be stacked on the top of the outgoing slide layer. According to the kind of transition, this order may be not suitable.

A fixed property z-index: -1 is helpful to send the incoming slide layer to back.

A fixed z-index: 1 to the outgoing slide (send to front) is also getting the same result, but currently setting a positive number to z-index may bring animation jank in Chrome.

Change layer order during a transition

If you want to swap the order of layers during animation, try to animate z-index property.

  1. @keyframes marp-incoming-transition-swap {
  2. /* Incoming slide will swap from back to front at 50% of animation */
  3. from { z-index: -1; }
  4. to { z-index: 0; }

  5. /* Declarations for moving animation */
  6. 0% { transform: translateX(0); }
  7. 50% { transform: translateX(50%); }
  8. 100% { transform: translateX(0); }
  9. }

  10. @keyframes marp-outgoing-transition-swap {
  11. 0% { transform: translateX(0); }
  12. 50% { transform: translateX(-50%); }
  13. 100% { transform: translateX(0); }
  14. }

z-index is always taking an integer value, and interpolated z-index value by animation does not take any decimal points too. So animating from z-index: -1 to z-index: 0 is exactly meaning to set -1 at the first half of duration and 0 at the last half, except if using a non-linear easing function.

Frequently used properties in transition

There are a lot of animatable CSS properties, and the following properties are frequently animated in built-in transitions.

Try it!

Transitions for Marp CLI's bespoke template backed by View Transitions API in the browser, provides flexibility to design your talk as you like. Custom transition brings out your boundless creativity, without complex JS codings, just declarative definitions in CSS.

We are really looking forward to what creative transition effects our community will create!

Share the custom transition you've made with Marp community. You can provide custom theme CSS including a bunch of custom transitions too.