| 1 | --- |
| 2 | title: 'Marp CLI: How to make custom transition' |
| 3 | date: 2022-05-28 |
| 4 | description: Marp CLI v2.4.0+ and Marp for VS Code v2.5.0+ have a stable support for page transitions with many useful built-in effects. But if you had not satisfied with any effects? Make your effects with CSS! |
| 5 | author: Yuki Hattori |
| 6 | github: yhatt |
| 7 | image: /og-images/how-to-make-custom-transition.jpg |
| 8 | --- |
| 9 | |
| 10 | [readme]: https://github.com/marp-team/marp-cli/blob/main/docs/bespoke-transitions/README.md |
| 11 | [built-in]: https://github.com/marp-team/marp-cli/blob/main/docs/bespoke-transitions/README.md#built-in-transitions |
| 12 | [view transitions api]: https://www.w3.org/TR/css-view-transitions-1/ |
| 13 | |
| 14 | **[Marp CLI v2](/blog/202205-ecosystem-update#marp-cli-v2)** has supported [brand-new page transitions for the `bespoke` HTML template](/blog/202205-ecosystem-update#slide-transition-experiment). You can use this stable transition support in either Marp CLI v2.4.0+ or Marp for VS Code v2.5.0+. |
| 15 | |
| 16 | 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][built-in] out of the box, by [just a simple definition `transition` directive](https://github.com/marp-team/marp-cli/blob/main/docs/bespoke-transitions/README.md#transition-local-directive). |
| 17 | |
| 18 | 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. |
| 19 | |
| 20 | <!-- more --> |
| 21 | |
| 22 | ### Index |
| 23 | |
| 24 | This article will describe the following things: |
| 25 | |
| 26 | 1. **[The anatomy of a transition](#the-anatomy-of-a-transition)**: How the transition effect will work in Marp |
| 27 | 1. **[Declare custom transitions](#declare-custom-transitions)**: How to register custom transitions by CSS |
| 28 | 1. **[Helpful tips for making your transition](#tips)** |
| 29 | |
| 30 | [See also the official documentation about transitions in Marp CLI.][readme] |
| 31 | |
| 32 | _If using [built-in transitions made by us][built-in] was enough, you don't need to read this article._ Please save your time, with keeping enjoying our transitions in your Markdown slide! :) |
| 33 | |
| 34 | > 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](https://developer.mozilla.org/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions). |
| 35 | |
| 36 | # The anatomy of a transition |
| 37 | |
| 38 | The first what the custom transition author has to know is "How the page transition effect is realized in a presentation slide". |
| 39 | |
| 40 | 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. |
| 41 | |
| 42 |  |
| 43 | |
| 44 | 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. |
| 45 | |
| 46 | 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. |
| 47 | |
| 48 | If you could figure them out, you probably also grasp that you have to respect the following 2 principles: |
| 49 | |
| 50 | - **The outgoing slide** should have **an animation to hide** the slide. |
| 51 | - **The incoming slide** should have **an animation to show** the slide. |
| 52 | |
| 53 | If either or both was not respected in a transition effect, it would become a weird transition. |
| 54 | |
| 55 | > Marp CLI's `bespoke` template will make two slide layers when navigated, and apply suitable animation keyframes declared in CSS. |
| 56 | |
| 57 | # Declare custom transitions |
| 58 | |
| 59 | ## Simple keyframe declaration |
| 60 | |
| 61 | Let's get started with a simple keyframe declaration for [the dissolve effect (also known as the cross-fade effect)](<https://en.wikipedia.org/wiki/Dissolve_(filmmaking)>), to learn how to set custom transition animation. Marp uses [standard syntax for CSS animation `@keyframes`](https://developer.mozilla.org/docs/Web/CSS/@keyframes) to declare transitions. |
| 62 | |
| 63 | When applying the dissolve effect to transition principles, you can derive that the effect needs these animations: |
| 64 | |
| 65 | - The outgoing slide has an animation to **decrease opacity from 100% to 0%**. |
| 66 | - The incoming slide has an animation to **increase opacity from 0% to 100%**. |
| 67 | |
| 68 | There are opposite changes with each other. In this case, you can define animations for both slide layers by one `@keyframes` declaration. |
| 69 | |
| 70 | First, declare `@keyframes` at-rule with the conventional name specified by Marp in your Markdown. |
| 71 | |
| 72 | ```markdown |
| 73 | --- |
| 74 | transition: dissolve |
| 75 | style: | |
| 76 | @keyframes marp-transition-dissolve { |
| 77 | /* ... */ |
| 78 | } |
| 79 | --- |
| 80 | |
| 81 | # Slide 1 |
| 82 | |
| 83 | --- |
| 84 | |
| 85 | <!-- _class: invert --> |
| 86 | |
| 87 | # Slide 2 |
| 88 | ``` |
| 89 | |
| 90 | **`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`. |
| 91 | |
| 92 | > This example is using [`style` global directive](https://marpit.marp.app/directives?id=tweak-theme-style) to declare keyframes. Of course, you also can use [the inline `<style>` element](https://marpit.marp.app/theme-css?id=tweak-style-through-markdown) or [custom theme CSS](https://marpit.marp.app/theme-css) to declare. |
| 93 | |
| 94 | 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. |
| 95 | |
| 96 | ```css |
| 97 | @keyframes marp-transition-dissolve { |
| 98 | from { |
| 99 | opacity: 1; |
| 100 | } |
| 101 | to { |
| 102 | opacity: 0; |
| 103 | } |
| 104 | } |
| 105 | ``` |
| 106 | |
| 107 | > This example has been declared `from` keyframe for clarity, but you can omit it because `opacity: 1` is a default style. |
| 108 | |
| 109 | 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](https://github.com/marp-team/marp-cli#preview-window---preview---p). |
| 110 | |
| 111 | ```bash |
| 112 | npx @marp-team/marp-cli@^2.4.0 --preview ./transition.md |
| 113 | ``` |
| 114 | |
| 115 | You have made the first custom transition! |
| 116 | |
| 117 |  |
| 118 | |
| 119 | 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](https://jakearchibald.com/2021/dom-cross-fade/). |
| 120 | |
| 121 | ## Split animations into outgoing and incoming |
| 122 | |
| 123 | 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. |
| 124 | |
| 125 | For example, the slide up effect must have these animations: |
| 126 | |
| 127 | - The outgoing slide should **move from the viewport to the upper outer**. |
| 128 | - The incoming slide should **move from the lower outer to the viewport**. |
| 129 | |
| 130 | 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`**. |
| 131 | |
| 132 | ```markdown |
| 133 | --- |
| 134 | transition: slide-up |
| 135 | style: | |
| 136 | @keyframes marp-outgoing-transition-slide-up { |
| 137 | from { transform: translateY(0%); } |
| 138 | to { transform: translateY(-100%); } |
| 139 | } |
| 140 | @keyframes marp-incoming-transition-slide-up { |
| 141 | from { transform: translateY(100%); } |
| 142 | to { transform: translateY(0%); } |
| 143 | } |
| 144 | --- |
| 145 | |
| 146 | # Slide 1 |
| 147 | |
| 148 | --- |
| 149 | |
| 150 | <!-- _class: invert --> |
| 151 | |
| 152 | # Slide 2 |
| 153 | ``` |
| 154 | |
| 155 | Unlike the simple transition, there is no auto-reversed animation in the incoming slide. Each animation should define in the right direction. |
| 156 | |
| 157 |  |
| 158 | |
| 159 | ## Transition for backward navigation |
| 160 | |
| 161 | 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. |
| 162 | |
| 163 |  |
| 164 | |
| 165 | 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. |
| 166 | |
| 167 | We are providing several solutions to deal with this. |
| 168 | |
| 169 | ### `--marp-transition-direction` CSS variable |
| 170 | |
| 171 | While playing transition, `--marp-transition-direction` [CSS custom property (as known as CSS variables)](https://developer.mozilla.org/docs/Web/CSS/Using_CSS_custom_properties) will be available in `@keyframes`. |
| 172 | |
| 173 | It provides `1` in forwarding navigation, or `-1` in backward navigation. Using [`var(--marp-transition-direction)`](https://developer.mozilla.org/docs/Web/CSS/var) together with [`calc()`](https://developer.mozilla.org/docs/Web/CSS/calc) function would be useful to calculate the position in response to the direction of slide navigation. |
| 174 | |
| 175 | <!-- prettier-ignore-start --> |
| 176 | |
| 177 | ```css |
| 178 | @keyframes marp-outgoing-transition-slide-up { |
| 179 | from { transform: translateY(0%); } |
| 180 | to { transform: translateY(calc(var(--marp-transition-direction, 1) * -100%)); } |
| 181 | } |
| 182 | @keyframes marp-incoming-transition-slide-up { |
| 183 | from { transform: translateY(calc(var(--marp-transition-direction, 1) * 100%)); } |
| 184 | to { transform: translateY(0%); } |
| 185 | } |
| 186 | ``` |
| 187 | |
| 188 | <!-- prettier-ignore-end --> |
| 189 | |
| 190 | And now, the slide-up custom transition is working completely in both directional navigation! |
| 191 | |
| 192 |  |
| 193 | |
| 194 | > NOTE: Any other CSS variables defined in the context of animation keyframes cannot use in keyframes. |
| 195 | |
| 196 | ### Set custom animations for backward transition |
| 197 | |
| 198 | Alternatively, you also can set more animation keyframes that are specific for backward navigation. |
| 199 | |
| 200 | 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. |
| 201 | |
| 202 | <!-- prettier-ignore-start --> |
| 203 | |
| 204 | ```css |
| 205 | @keyframes marp-incoming-transition-triangle { |
| 206 | /* Wipe effect from left top */ |
| 207 | from { clip-path: polygon(0% 0%, 0% 0%, 0% 0%); } |
| 208 | to { clip-path: polygon(0% 0%, 200% 0%, 0% 200%); } |
| 209 | } |
| 210 | |
| 211 | @keyframes marp-incoming-transition-backward-triangle { |
| 212 | /* Wipe effect from right bottom */ |
| 213 | from { clip-path: polygon(100% 100%, 100% 100%, 100% 100%); } |
| 214 | to { clip-path: polygon(-100% 100%, 100% -100%, 100% 100%); } |
| 215 | } |
| 216 | ``` |
| 217 | |
| 218 | <!-- prettier-ignore-end --> |
| 219 | |
| 220 | In backward navigation, each layer will try to use the backward keyframes first, and fall back to the normal keyframes if not declared. To disable unintended fallback in backward animations, set an empty declaration of `@keyframes`. |
| 221 | |
| 222 | <!-- prettier-ignore-start --> |
| 223 | |
| 224 | ```css |
| 225 | @keyframes marp-outgoing-transition-zoom-out { |
| 226 | from { transform: scale(1); } |
| 227 | to { transform: scale(0); } |
| 228 | } |
| 229 | @keyframes marp-incoming-transition-zoom-out { |
| 230 | /* Send the incoming slide layer to back */ |
| 231 | from { z-index: -1; } |
| 232 | to { z-index: -1; } |
| 233 | } |
| 234 | |
| 235 | /* ⬇️ Declare empty keyframes to disable fallback ⬇️ */ |
| 236 | @keyframes marp-outgoing-transition-backward-zoom-out {} |
| 237 | @keyframes marp-incoming-transition-backward-zoom-out { |
| 238 | from { transform: scale(0); } |
| 239 | to { transform: scale(1); } |
| 240 | } |
| 241 | ``` |
| 242 | |
| 243 | <!-- prettier-ignore-end --> |
| 244 | |
| 245 | OK, I've described all about declarations for the custom transition! |
| 246 | |
| 247 | # Tips |
| 248 | |
| 249 | ## Easing function |
| 250 | |
| 251 | Each transition has a linear easing by default. You can specify [`animation-timing-function` property within individual keyframes](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#:~:text=A%20keyframe%27s%20timing%20function%20is%20applied%20on%20a%20property%2Dby%2Dproperty%20basis%20from%20the%20keyframe%20on%20which%20it%20is%20specified%20until%20the%20next%20keyframe%20specifying%20that%20property%2C%20or%20until%20the%20end%20of%20the%20animation%20if%20there%20is%20no%20subsequent%20keyframe%20specifying%20that%20property) if you want. |
| 252 | |
| 253 | > Setting [`animation-timing-function: step-end;`](https://developer.mozilla.org/docs/Web/CSS/animation-timing-function#step-end) to a keyframe can make paused animation until the next keyframe. |
| 254 | |
| 255 | ## Duration |
| 256 | |
| 257 | 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%`). |
| 258 | |
| 259 | <!-- prettier-ignore-start --> |
| 260 | |
| 261 | ```css |
| 262 | @keyframes marp-incoming-transition-gate { |
| 263 | from { |
| 264 | /* Set the default duration of the "gate" transition as 1 second. */ |
| 265 | --marp-transition-duration: 1s; |
| 266 | |
| 267 | clip-path: inset(0 50%); |
| 268 | } |
| 269 | to { clip-path: inset(0); } |
| 270 | } |
| 271 | |
| 272 | @keyframes marp-outgoing-transition-backward-gate { |
| 273 | from { |
| 274 | /* You also can set a different default for backward transition as necessary. */ |
| 275 | /* --marp-transition-duration: 1.5s; */ |
| 276 | |
| 277 | clip-path: inset(0); |
| 278 | } |
| 279 | to { clip-path: inset(0 50%); } |
| 280 | } |
| 281 | @keyframes marp-incoming-transition-backward-gate { |
| 282 | from { z-index: -1; } |
| 283 | to { z-index: -1; } |
| 284 | } |
| 285 | ``` |
| 286 | |
| 287 | <!-- prettier-ignore-end --> |
| 288 | |
| 289 | The slide author can override the default duration at any time, through the `transition` local directive in Markdown (`<!-- transition: fade 2s -->`). |
| 290 | |
| 291 | ## Fixed property |
| 292 | |
| 293 | If some of the properties required a fixed value while playing transition, try to set the same declaration into `from` (0%) and `to` (100%). |
| 294 | |
| 295 | <!-- prettier-ignore-start --> |
| 296 | |
| 297 | ```css |
| 298 | @keyframes marp-outgoing-transition-pin { |
| 299 | /* Use fixed transform-origin */ |
| 300 | from { |
| 301 | transform-origin: top left; |
| 302 | animation-timing-function: ease-in; |
| 303 | } |
| 304 | to { |
| 305 | transform-origin: top left; |
| 306 | transform: rotate(90deg); |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | @keyframes marp-incoming-transition-pin { |
| 311 | /* Send the incoming slide layer to back */ |
| 312 | from { z-index: -1; } |
| 313 | to { z-index: -1; } |
| 314 | } |
| 315 | ``` |
| 316 | |
| 317 | <!-- prettier-ignore-end --> |
| 318 | |
| 319 | ## Layer order |
| 320 | |
| 321 | [As presented in a diagram earlier](#the-anatomy-of-a-transition), 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. |
| 322 | |
| 323 | A fixed property [`z-index: -1`](https://developer.mozilla.org/docs/Web/CSS/z-index) is helpful to send the incoming slide layer to back. |
| 324 | |
| 325 | > 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. |
| 326 | |
| 327 | ## Change layer order during a transition |
| 328 | |
| 329 | If you want to swap the order of layers during animation, try to animate `z-index` property. |
| 330 | |
| 331 | <!-- prettier-ignore-start --> |
| 332 | |
| 333 | ```css |
| 334 | @keyframes marp-incoming-transition-swap { |
| 335 | /* Incoming slide will swap from `back` to `front` at 50% of animation */ |
| 336 | from { z-index: -1; } |
| 337 | to { z-index: 0; } |
| 338 | |
| 339 | /* Declarations for moving animation */ |
| 340 | 0% { transform: translateX(0); } |
| 341 | 50% { transform: translateX(50%); } |
| 342 | 100% { transform: translateX(0); } |
| 343 | } |
| 344 | |
| 345 | @keyframes marp-outgoing-transition-swap { |
| 346 | 0% { transform: translateX(0); } |
| 347 | 50% { transform: translateX(-50%); } |
| 348 | 100% { transform: translateX(0); } |
| 349 | } |
| 350 | ``` |
| 351 | |
| 352 | <!-- prettier-ignore-end --> |
| 353 | |
| 354 | `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. |
| 355 | |
| 356 | ## Frequently used properties in transition |
| 357 | |
| 358 | [There are a lot of animatable CSS properties](https://developer.mozilla.org/docs/Web/CSS/CSS_animated_properties), and the following properties are frequently animated in built-in transitions. |
| 359 | |
| 360 | - [`opacity`](https://developer.mozilla.org/docs/Web/CSS/opacity) |
| 361 | - [`transform`](https://developer.mozilla.org/docs/Web/CSS/transform) |
| 362 | - [`filter`](https://developer.mozilla.org/docs/Web/CSS/filter) |
| 363 | - [`clip-path`](https://developer.mozilla.org/docs/Web/CSS/clip-path) |
| 364 | - [`mask-image`](https://developer.mozilla.org/docs/Web/CSS/mask-image) (`-webkit-mask-image`) |
| 365 | - [`box-shadow`](https://developer.mozilla.org/docs/Web/CSS/box-shadow) |
| 366 | - [`z-index`](https://developer.mozilla.org/docs/Web/CSS/z-index) |
| 367 | |
| 368 | # Try it! |
| 369 | |
| 370 | 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. |
| 371 | |
| 372 | We are really looking forward to what creative transition effects our community will create! |
| 373 | |
| 374 | Share the custom transition you've made with [Marp community](https://github.com/orgs/marp-team/discussions). You can provide custom theme CSS including a bunch of custom transitions too. |
| 375 |