HTML animation to GIF

Two routes. Screen record the page and convert, which takes a minute and covers every animation. Or capture frames in the browser, which is exact and only works for animations you control in code.

To turn an HTML animation into a GIF you either screen record the page and convert the video, or capture frames in the browser and encode them. No browser exports a GIF from a running page on its own.

Pick by what the animation is. CSS keyframes and third party widgets go the recording route. A canvas or a scripted timeline you wrote yourself can be captured exactly.

A CSS animation playing in a browser window, part way through its keyframe sequence.
A CSS animation playing in a browser window, part way through its keyframe sequence.

The two routes compared

Screen record Capture frames in code
Works with CSS keyframes Yes Only if driven from script
Timing accuracy Whatever the recorder captured Exact, frame by frame
Resolution Screen resolution Any size you set
Setup time About a minute An hour the first time
Dropped frames Possible under load None

Route one: record and convert

  1. Set the browser window to the size you want the GIF to be. Recording large and shrinking later softens text.
  2. Record the region with the operating system recorder, or the browser dev tools recorder.
  3. Trim the clip to the loop point so the GIF repeats cleanly.
  4. Convert to GIF with a palette pass, which is what keeps colours from banding.
ffmpeg -i clip.mp4 -vf "fps=12,scale=640:-1:flags=lanczos,palettegen" palette.png
ffmpeg -i clip.mp4 -i palette.png -lavfi "fps=12,scale=640:-1:flags=lanczos [x]; [x][1:v] paletteuse" out.gif

Two passes, because GIF allows 256 colours per frame and letting the encoder choose them from your actual footage is the difference between clean and muddy.

The numbers to change are fps and the width in scale. Start at 12 and 640.

Route two: capture frames in the browser

Only viable when you can step the animation. That means a canvas render loop, or a timeline you advance yourself.

const frames = [];
const total = 36;

for (let i = 0; i < total; i++) {
  drawScene(ctx, i / total);          // your render, given a 0 to 1 progress
  frames.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
}

Then hand the frames to a GIF encoder library. The important part is that drawScene takes progress as an argument rather than reading a clock, which also makes the animation testable.

For a non canvas page, capture each step as an image of the rendered DOM instead. That is what HTML to image does for a single frame, repeated once per step.

A canvas animation with a progress control, stepped to a specific frame rather than playing.
A canvas animation with a progress control, stepped to a specific frame rather than playing.

Stepping a CSS animation

Pure CSS keyframes cannot be paused at an arbitrary point from the outside in a reliable way. Move the animation to the Web Animations API and you get a seekable object.

const anim = el.animate(
  [{ transform: 'translateY(24px)', opacity: 0 },
   { transform: 'translateY(0)',    opacity: 1 }],
  { duration: 600, easing: 'cubic-bezier(.2,.7,.3,1)', fill: 'both' }
);
anim.pause();
anim.currentTime = 300;   // exactly half way

This is also how animation on page load becomes testable, since you can hold it at any point and look at it.

Making the loop seamless

A GIF that jumps at the loop point draws attention to itself. Two ways to avoid it.

  • End where you began. Design the animation so the last frame matches the first, then drop the duplicate final frame before encoding.
  • Hold at the end. Add a half second of the final state and let the restart read as deliberate rather than as a stutter.

For an interface clip, the second is usually better. Readers need a moment on the end state to understand what happened, and a tight loop denies them that.

Set the loop count explicitly if the destination respects it. Infinite is the default and is right for a short clip. A long one that loops forever is a distraction beside text.

Keeping the file small

GIF has no motion compression. Every frame is stored whole, so the levers are blunt and effective.

  • Dimensions. Halving both sides cuts the file to roughly a quarter.
  • Frame rate. 12 frames per second reads as smooth for interface motion. 30 is video habit.
  • Duration. Three seconds that loop beats twelve seconds that play once.
  • Colours. Flat interface colours compress well. Gradients and shadows do not.
  • Crop. Record the component, not the whole browser window with its toolbars.

A three second, 640 pixel wide, 12 frame per second interface clip usually lands between 1 and 3 megabytes. A full screen 30 frame recording of the same thing can be ten times that.

Where GIFs still make sense

Not a large list, and worth checking before spending the time.

  • Ticket and issue systems that accept image attachments but not video.
  • Older email clients, where a GIF animates and a video does not play at all.
  • Documentation that has to work offline as a single folder of files.
  • Chat surfaces that autoplay images inline but put video behind a click.

Everywhere else, an MP4 or WebM of the same clip is smaller and sharper, and a link to the live page beats both.

Accessibility of a moving image

An animation the reader cannot pause is a problem for some people, and a GIF has no controls at all.

  • Keep it under five seconds if it loops, or make it play once.
  • Avoid rapid flashing. More than three flashes per second is a known seizure risk.
  • Give it real alt text describing what happens, not the file name. See alt text.
  • Put the same information in the surrounding words, so nothing depends on watching it.

A video element with controls solves the pause problem outright, which is another reason to prefer it where it is accepted.

When not to make a GIF at all

If the destination accepts a link, send the page. A GIF is a lossy 256 colour copy of something that already runs everywhere, and it cannot be corrected without redoing the whole export.

The animated page pasted into a NOS document, running at full quality with its own address.
The animated page pasted into a NOS document, running at full quality with its own address.

Paste the HTML into a NOS document and it renders with the animation running, at full colour and full resolution, on any device. Copy the share link and send that.

Editing a duration later is an edit to the page, and the link already points at the new version. A GIF sent last week is still the old timing, in a thread nobody scrolls back through.

The comparison in screenshot against live page applies here with more force, since a GIF loses colour depth and timing on top of everything a screenshot loses. Keep the GIF for surfaces that genuinely accept images only.

Questions people ask

Can a browser export an HTML animation as a GIF directly?

No. There is no browser feature that writes a GIF from a running page. You either record the screen and convert the video, or capture frames yourself with a canvas and encode them with a JavaScript GIF library.

Why is my GIF so large?

GIF stores every frame with a 256 colour palette and no motion compression, so file size grows with duration, dimensions and frame rate. Cutting the size in half roughly quarters the file. Dropping from 30 to 12 frames per second helps almost as much.

Should I use a GIF or a video?

MP4 or WebM is smaller and sharper for the same clip, often by a large margin, and every modern surface plays it. Use GIF when the destination only accepts images, for example some ticket systems and older email clients.

How do I capture a CSS animation frame by frame?

Drive it from the Web Animations API or a JavaScript timeline rather than pure CSS, set the current time frame by frame, and capture after each step. A pure CSS keyframe animation cannot be stepped, so screen recording is the practical route.

Keep reading