Over the last few weeks I’ve been vibe-coding an animation tool for Penpot: keyframe animation for boards, right in the workspace, in the spirit of Figma Motion. It lives on a branch of my fork behind a feature flag. I’d love for you to try it and tell me what works and what doesn’t.
What it does
Timeline
Each board gets its own timeline in a dock at the bottom (View → Show animation timeline, or Shift+M for motion mode).
You can keyframe position, size, rotation, scale, opacity, corner radii, fill and stroke color/opacity, stroke width, shadows, layer and background blur, and path trim (strokes that draw themselves).
Easing presets, a cubic-bezier editor, back easings, springs (gentle, quick, bouncy, slow, custom) and hold.
Playback modes: once, loop, ping-pong.
Preset animations (fade, move, scale, rotate, slide, pop, zoom, spin, …) stay editable. Applied to several layers, they get a stagger.
Markers, snapping, box selection, copy/paste of keyframes and arrow-key nudging.
Editing on the canvas
In motion mode the canvas shows the board at the playhead, and you edit shapes where they appear. Each drag, resize or rotation becomes a keyframe.
REC / auto-keyframe records every property you change at the playhead.
Motion paths: the path of a moving layer shows on the canvas, with handles to curve it.
The design tab shows the values at the playhead. Typing a value there sets the keyframe.
Components & collaboration
Copies of an animated component play the animation of their main, also in other boards and when resized.
Time-stamped comments: a comment placed in motion mode belongs to a moment of the animation, and a click on it moves the playhead there.
Edits to different layers merge, so two people can animate the same board. Animations survive duplicating, copy/paste and moving layers between boards.
View mode plays the animations, and animated boards get a play button on their title.
Export & import
Export a board as MP4, WebM, GIF, animated AVIF, animated SVG, Lottie (vectors, gradients, strokes, trim, shadows, blur, clipping) or CSS @keyframes (respects prefers-reduced-motion). Inspect shows the animation CSS of the selection.
Import Lottie (JSON or dotLottie, by drag & drop or File → Import Lottie…) as a board with its timeline. A warning lists what it could not bring over.
Plugins & MCP
The plugin API, and through it the Penpot MCP server, can create timelines, keyframes, presets and markers, and set the export settings. So an AI agent can animate a board for you.
Performance
Works with both renderers, but the WASM renderer is the target. It now draws the viewport in one pass during playback. A toggle plays in full quality (shadows and blur) instead.
Check out the branch and start the devenv (./manage.sh run-devenv).
Add enable-feature-animation to the flags (penpotFlags in frontend/resources/public/js/config.js). Turn on the WASM renderer for the video exports. (Optional also add enable-available-viewer-wasm to the flags)
Select a board and open View → Show animation timeline (or press Shift+M).
This is experimental. The data model can still change, so please don’t use it on files you care about.
Not there yet
Shape morphing (animating path points). The Lottie import also skips track mattes, merge paths and text layers.
Text-specific animation (per word or letter).
Without the WASM renderer, the only export is Lottie, and only when all layers are vectors.
Thx @diacritica, in short, Motion extends Penpot’s format rather than adding a second one:
All animation data sits in one optional page key, :timelines, next to :flows and :guides. There is one timeline per board, keyed by the board’s id. Each timeline has one track per animated layer, keyed by the layer’s id. Each track holds keyframes (one property, one time, one value, one easing) and editable presets such as fade or move.
Shapes don’t change: no new shape attributes, no file migration. A layer’s own attributes are its rest state. Playback turns the timeline into Penpot’s normal modifiers, the ones used while dragging, so both renderers draw it and nothing is written back.
Edits use three new change types built like :set-flow, so undo, multi-user sync and version history work as for any other edit.
animation/v1 is a frontend-only feature and is never stored on a file. A build without Motion opens animated files, shows the static design and leaves the key alone.
Good news: part of this is already there. In the prototype (View mode), an animated board starts playing on its own when it’s shown, for example when you get there with a “Navigate to” interaction on “On click” or “Mouse enter”. If you don’t want it to start by itself, you can turn off autoplay in the timeline.
What’s still missing is a “play this animation” action that you can attach to any trigger (click, hover, etc.) without switching boards, plus real hover states. For now you can fake a hover with an overlay that opens on “Mouse enter” and closes on “Mouse leave”, and the board underneath keeps playing its animation. But it’s a great idea, and I’m noting it down!
And about the DIV: you really don’t need it Motion is made for designers. You do keyframes, presets and easing on the canvas and in the timeline, without writing any code. The CSS and Lottie export is just an extra for anyone who wants to use the animation in a website or an app.
Please keep going with yours! There’s a lot of room next to Motion. 3D, for example: Motion is 2D only for now, so 3D would be a great addition from your side. The same goes for anything beyond keyframes, like physics or animations driven by code.
A lot has landed in the ten days since my post, so here is the short version.
A seven-slide presentation built in Penpot, where every slide animates and has sound: first the timeline in the workspace, then the prototype clicked through.
Sound
Drop an audio file (MP3, WAV, Ogg, M4A, WebM, FLAC) on a board or on the timeline. A clip shows its waveform, and you can move, trim, fade and split it (Cmd/Ctrl+B), change its speed, and set its volume up to 400 %.
The sound plays in sync in the workspace, in view mode and on share links. MP4 and WebM exports include it.
Copies of an animated component play the sound of their main.
Text animation (this was on my “not there yet” list)
Animate a text by characters, words or lines. You set how far apart the units start and in which order: forward, backward, from the center, from the edges or random. The text stays editable.
New text styles: Typewriter, Rise words, Cascade lines and Pop letters. There’s also a new Blur in / Blur out preset for any layer.
Animation styles, copy and paste
Save what a layer plays (presets and keyframes) as a style, then apply it to other layers, staggered if you pick several. Edit the style once and every layer that uses it follows.
Styles sit in the assets panel and work through shared libraries, with the usual “updates available” flow.
Copy animation and Paste animation (right-click → Copy/Paste as) move an animation from one layer to others, even into another file. Each layer plays it from its own position.
Motion tokens and easing
New token types: duration, cubic bezier and spring (DTCG). Bind them to the timing and easing of presets, styles and keyframes. Change a token or switch a theme, and the animations follow.
Presets can now take a custom bezier curve or a custom spring, from a menu.
Prototypes and view mode
A board plays its animation when the prototype shows it, like a slide. A toggle on the timeline turns this off.
Playback runs in real time. With the WASM renderer it reaches 60 fps, push/slide/dissolve transitions run smoothly, and each slide’s sound starts as the slide comes in.
Hover overlays no longer stop the board under them, and looping component copies keep running after the board’s animation ends.
Inspect
A Motion tab shows the animation of the selection: a read-only timeline, plus the code as CSS, JSON or React (motion.dev). Split text becomes one span per character or word.
Export
Exports run in a tab of their own, so the workspace stays responsive. The tab shows each frame as it’s captured and plays the file when it’s done.
New: dotLottie, transparent backgrounds in WebM, GIF and AVIF, any size typed (2.5x, 512w, 720h), and several boards at once into one ZIP.
Fixed: exports are cut to the board, still layers no longer jitter by a pixel, and video colors match the board (they used to come out too dark and blue).
Small things
Tap Space to play or stop the selected animated board, even with the timeline closed. Holding Space still pans.
Drag the end of the timeline on its background. Each board keeps its own playhead.
Plugins and MCP
The plugin API, and with it the MCP server, covers all of this: audio clips, split text, animation styles, motion tokens, copy animation, autoplay, export sizes, and the timeline as JSON, React or CSS.
First of all, this is incredible, @girafic. I love it.
I actually got a little emotional seeing a timeline inside Penpot, honestly. My editor and motion designer heart is very happy.
I still need to spend some real time with it, but I’ve already got it running. I’d love to contribute here. My animation background comes from other places: After Effects and Blender, mostly. I’m looking around to see how it works.
I can’t find the exporter. Did you say it opens in a separate window?
I’m not even sure this is possible, but as you’ll see, a timeline with a lot of layers gets complicated pretty quickly. Could we think about a way to view the timeline fullscreen, with the animated layers? Right now I can only make the timeline taller, up to about half my window… and I think pretty soon that won’t be enough.
It’s a small thing. I wish I could come up with an idea that actually makes more sense. Thank you so much!!
Thank you so much It means a lot to hear that from an editor and motion designer, and After Effects and Blender experience is exactly what this needs.
The exporter: yes, it opens in a new browser tab, but you start it from the right sidebar, not from the timeline. Select the animated board itself (just the board, not the layers inside it). The Export section of the Design tab then has two tabs, Static and Animated. Under Animated you pick the format, size, frame rate and so on. When you click Export, a new tab renders the animation and downloads it, so the workspace stays responsive. Two things can hide it:
Most formats need the WASM renderer. Without it, the Animated tab only offers Lottie (and only when every layer is a vector), or doesn’t show at all. To turn it on, add &wasm=true to the workspace URL, or enable-feature-render-wasm to the flags.
If your browser blocks pop-ups, the tab can’t open. After a few seconds the panel offers to open it again or to export right there.
Fullscreen timeline: not a small thing at all, you’re right. With many layers, half the window isn’t enough. For now a few things help: collapse or expand all layers, zoom the timeline with Ctrl/Cmd + scroll, and select a layer on the canvas or in the layers panel to jump to its row. But a way to maximize the timeline, like Ctrl+Space in Blender or the ` key in After Effects, would make a lot of sense. I’ll look into it.
@almu asked for a fullscreen timeline, so here it is:
Ctrl+Space (as in Blender), Alt+Shift+M or a button in the toolbar maximizes the timeline over the whole window, sidebars included. If your system uses Ctrl+Space to switch input languages, use Alt+Shift+M.
The canvas becomes a small preview window over the timeline. You can move it, resize it from its corners and hide it. It stays where you leave it.
Taller rows: a slider in the toolbar, or Alt + scroll, makes the rows 28 to 72 px tall.
Record a voice-over
A microphone button in the timeline records from the playhead while the animation plays. The clip shows up on its own audio lane right away, with a live waveform.
Recording options: a pre-roll of 1 to 3 s, a choice of microphone, and muting the other clips while you record.
The design tab now has an Audio section: mute, remove or add audio files. Double-click a file’s name to rename it.
Before zero
Keyframes, presets and audio clips can start before 0. That part doesn’t play, but it sets where the animation is at the start: a fade from −500 to 500 ms starts half done.
Animated export
Several boards at once: select them and export them into one ZIP.
A row says how many issues an export has (“5 issues to review”). A click lists what the format plays differently from the canvas.
Raster exports are cut to the board, and layers that stand still no longer shake by a pixel.
The export tab shows the board’s name and the progress in its title, so you can follow it from another tab. Its preview plays smoothly, and the Done page now plays Lottie and dotLottie too.
SVG export: real vectors
Layers are exported as vectors instead of one picture per layer: they stay sharp when scaled, files are much smaller, and texts keep their fonts.
Fills, strokes, corners, path trim, shadows and sizes animate too. Resizing a group resizes its layers instead of stretching them.
New options: Outline text, Include id attribute (layer names as ids) and Include bounding box.
Blur and shadows look like on the canvas, in Safari too. Inner shadows, shadow spread, uneven corners and masked groups work as well.
Lottie export
Texts are vectors (the outlines of their glyphs) instead of images, including texts split into characters, words or lines.
Rectangles, ellipses and boards resize around their pivot as on the canvas, and groups resize their layers.
Masked groups become mattes, and inner shadows and shadow spread are drawn as on the canvas.
Plugins & MCP
Read and set the pivot of a layer (Timeline.originOf, Timeline.setOrigin).
New export settings outlineText, includeIds and includeBoundingBox, custom sizes, renaming of audio files, and times before 0.
Fixed: sending a board with a timeline to a plugin’s UI hung the page.
Fixes
Inner shadows of groups now show on the canvas with the WASM renderer (they were only in exports).
An animated text color plays on the canvas, and the CSS and React code animate it as color (strokes as -webkit-text-stroke).
Animating a board’s scale scales its content too, and rotated shapes with an animated size resize along their own sides.
If you run the branch: after pulling, restart the backend (audio clips before 0 need the new schema), and reload nginx or restart the devenv so the browser lets the page use the microphone.
As always: it’s experimental, and feedback is very welcome!
I’ve been testing Penpot Motion for a few days and wanted to share what I found.
First, it’s really fun to use. I tried it on Linux and on Mac, and the overall feel is very promising.
What I like
Being able to edit groups makes it easy to animate “precomps”. That’s a nice side effect of how Penpot works. It would be even better if groups could be nested, or if there were something like precompositions, as in After Effects. That would also help keep the timeline lighter when a project grows.
The dopesheet-style timeline feels close to Blender’s. I don’t know how Figma’s motion tool works, so I can’t compare, but it’s a solid base
Bugs I found
Timeline jitter. Tested on Linux Fedora 44, chromium. It ran fine! but on Mac (M1, tahoe 26.5.2 - chrome and Firefox) I got an error. I’m attaching a recording. The timeline shakes a little, and only the timeline does. It happens in full screen and also in a “normal” sized window. I’m not sure about what triggers it, Just refresh doesn’t fix it.
Animated layer missing from the timeline. I had an animation on a layer that didn’t appear in the timeline because it wasn’t inside a board. That may be intended, but it surprised me. not sure it’s a bug.
Motion paths that stay visible. The lines disappear when I move an object from the numeric inputs or by dragging. It would help to keep seeing them while editing.
Per-property keyframe indicators. I’d like to see keyframe markers for X, Y, W, H and so on separately. They can be animated independently, so it’s odd not to see that in the designt tab.
Quick property filter. Blender’s dopesheet has a search field, and After Effects lets you press a key to show only one property (for example “P” for position). Either would help a lot when a layer has many keyframe types.
Frames as a time unit. Milliseconds and seconds are fine, but frames are the standard unit for video animation. A switch between ms and frames would help.
Ideas and reflections (not requests, just thoughts)
It feels closer to Blender’s dopesheet than to After Effects in my opinion. The timeline only shows animated layers. Which is cool/cleaner. But something I love in After Effects is masks: where one layer masks others. Mattes are SO useful in animation, not an easy thing in blender TBH, and I’d be curious whether they could fit here. (an screenshot from AE)
Linking one property to another (from another layer) like the AE pick whip idea, and making one object follow or be parented to another. Those could open a lot of possibilities later on.
I’m happy to test more or send files if that helps.
Right now the link to Issues in your GitHub repo takes me (in the description, not an issues tab available) to the issues list of the original Penpot repository, so I can’t report these bugs there. It looks like the usual behaviour for a fork, and I guess that’s not what you want. Once it points to the right place, or if you prefer another channel, tell me and I’ll file the bugs there.
Thank you so much for testing it this carefully, @almu Feedback like this helps a lot, and sorry for the slow reply: I wanted to have something to show for your last ideas too. Here’s what came out of it. Everything below is on the branch: GitHub - girafic/penpot at girafic-penpot-timeline-animation · GitHub
Bugs
Timeline jitter: found it, thanks to your recording. It happens with scrollbars that take up room (macOS with a mouse, or “Show scroll bars: Always”) and a browser zoom other than 100%. The timeline rounded its width to whole pixels and came out a fraction of a pixel too wide. The scrollbars appeared, the timeline shrank, the scrollbars went away, and so on, on every frame. It’s fixed now. Please tell me if you still see it on your Mac.
Animated layer missing from the timeline: not intended. A layer dragged out of every board keeps its animation for when it goes back in, and it kept playing, but you couldn’t see or edit it. Such layers now show up after the layers of their board, marked “Outside the board”. View mode and the exports leave them out, and the tooltip says so.
Your proposals
All four are in:
Motion paths stay visible while you drag a layer or change its fields. While you drag, the path follows the layer as the drop will record it.
One keyframe diamond per field: X, Y, W, H and the two scales each have their own, and each one keys only its own property.
A filter field over the rows of the timeline, like the search in Blender’s dopesheet: “pos y” shows Position Y, “dot pos” the position of the layer Dot. Esc clears it. Or press a key, as in AE: in the timeline, P, S, R and T show only position, scale, rotation or opacity. Shift adds a kind or takes it out, and U or the same key again shows everything.
Frames: the unit button now switches between ms, seconds and frames. Frames follow the frame rate of the board’s export (30 by default), so frame 45 in the timeline is frame 45 of the video. The playhead and dragged keyframes land on whole frames. Hold Ctrl/Cmd to place them freely.
Precomps
Your precomp idea led to this: a component copy in a board plays the animation of its main, and now the board can start it later and play it faster or slower, as with a precomposition. Select the copy: the Design tab shows Start (ms) and Speed (%), and you can drag its bar in the timeline too. The copy keeps that timing when you duplicate, paste or move it.
Masks and mattes
They’re here! A masked group now has a mask mode in the Design tab, the four modes of AE’s track mattes: Alpha (as before), Alpha inverted, Luminance and Luminance inverted.
They work with both renderers and in the SVG and Lottie exports. One limit: lottie-web can’t play “luminance inverted”, and the export panel tells you.
Lottie files with track mattes now import with them, as masked groups.
Plugins can read and set the mode too (group.maskMode).
Linking and parenting
Your last idea is in too:
Follow: a layer can follow another layer of its board. In the Follow section of the Motion tab, choose the leader in a list, or pick it on the canvas with a click, much like a pick whip. The follower moves, turns and scales with its leader around the leader’s pivot, as a child does, and can take its opacity as well. It can lag behind by a delay, and follow from the leader’s first frame or from where the leader rests. A follower can lead others, so chains work, and on ping-pong boards the followers lag behind on the way back too.
Reach (IK): the end of a chain of two follows, say upper arm, forearm and hand, can reach for a goal. The links turn so that the hand lands on it, with the elbow bending the way you choose. “New goal” puts a ring on the layer that you can animate; the exports leave the ring out.
Lottie: where Lottie plays a follow the same way, the export makes the follower a child of its leader (Lottie’s parenting), and Lottie files with parents come in as follows.
Linking one property to another, as the pick whip does with an expression, isn’t there yet. Follow covers the usual case of one layer moving with another.
Also new: path morphing and animated SVG import
A path can morph into another outline: in motion mode, edit the path at the playhead and its Path row gets a keyframe. It plays on the canvas, in videos and in the SVG and Lottie exports, and it imports from Lottie files and animated SVGs.
Animated SVGs with SMIL or CSS animations now come in as a board that plays them: drop one on the canvas, or use File > Import animation…. Spinners, loaders, lines that draw themselves, shapes that morph and the like arrive with their timeline. A warning names what a board can’t play (clicks, filters). The SVG import in general got better along the way: masks, clip paths, gradients and dashes come in, and texts arrive as editable Penpot texts, turned and sized as in the SVG.
Issues on GitHub
You were right, the fork had no Issues tab. It has one now: Issues · girafic/penpot · GitHub. Please file bugs and ideas there, with files if you have them. This thread works too.
If you run the branch: after pulling, restart the backend, as the mask modes, the timing of copies, follows and path keyframes need the new schema.