Manim
A few years ago,
Twitter user @38mo1,
a chemistry teacher from Japan,
posted a Halloween × Boolean logic meme
that I liked enough to save to ~/Downloads
.
And ever since,
I’ve had a yearly reminder every October 1st
to open the image and get a seasonal chuckle 🎃
More recently, I wanted to teach my kids some advanced snake maths (long story 🐍) and remembered that the software 3blue1brown uses for their videos was open-source.
Which is all to say that this year for Halloween, NSHipster is dressing up as a beloved YouTuber. How’d I do?
Neat, right? Everything used to create that video available on GitHub. Read on to learn more about how to get started on your next great explainer.
Back in 2015, Grant Sanderson found himself at a hackathon with a desire to practice his Python skills. What emerged was “very scrappy code for visualizing functions as transformations” — a playful experiment that would eventually become the foundation of both the 3Blue1Brown YouTube channel (now 6.5+ million subscribers strong) and Manim itself.
0 to 1
Manim has a reputation for being difficult to install.
Some of this is a natural consequence for a tool
whose audience extends to self-described non-programmers.
Some of this is intrinsic to working with motion graphics.
And being written in Python doesn’t do it any favors
(though uv
pretty much solves all the complaints anyone could have).
Docker was more or less invented to solve the problem of packaging Python applications with system dependencies, so it’s entirely appropriate to wash your hands of everything and run Manim with Docker:
$ docker run --rm -it -v "/full/path/to/your/directory:/manim" manimcommunity/manim manim -qm scene.py My Scene Name
…but that feels like giving up.
And besides, you’ll appreciate having a proper Python development environment once you’re up-and-running. Stick with me, and I promise everything will be A-OK (pinky swear 🤙).
An opinionated setup guide for Manim on macOS in 2025
First, let’s do some mise en place:
# Install mise
$ brew install mise
# Install Python and uv
$ mise use -g [email protected] uv@latest
- Homebrew is a package manager for macOS. It’s the best way to install the system dependencies we need to run Manim.
- Mise is a polyglot tool manager. While you could use Homebrew to install Python, you often need to keep a few versions around to work across different projects. So that’s what we’ll recommend here.
- uv is a Python package manager. It’s wicked fast and just works™.
Now, let’s create our project and make ourselves at home:
# Create a new project
$ uv init my-explainer
$ cd my-explainer
# Now open with your preferred $EDITOR
Next, go ahead and install Manim’s system dependencies:
# Install Manim dependencies
$ brew install pkg-config cairo # for graphics
$ brew install --cask mactex-no-gui # for ƒοrmυℓαѕ
$ brew install sox # for voiceovers
-
Cairo is a 2D graphics library.
You need
pkg-config
to get the Pycairo bindings working properly. -
MacTex is a LaTeX distribution for macOS.
You’ll need this to make a proper maths explainer with impressive-looking formulas.
We recommend downloading
mactex-no-gui
, which foregoes the GUI apps that ship in the full version. - SoX is a utility for converting audio files to different formats. Manim has some nice-_ish_ affordances for putting voice-overs into your scenes, and you’ll need SoX to take advantage of them.
Finally, let’s run a health check to see if we’re still on the happy path:
# Is everything working? (🫣)
$ uv run manim checkhealth
Huzzah!
Setting the Scene
The Manim API is rich and well-documented, with an extensive collection of examples. Rather than attempt to explain everything here, let’s look at one concrete example to get a sense of how it all works.
Here’s a standalone “Trick XOR Treat” scene:
from manim import *
class Trick XORTreat(Scene):
def construct(self):
# Panel background
panel_bg = Rounded Rectangle(width=7, height=5, corner_radius=0.2).set_fill(
GREY, opacity=1
)
# Title
title = Text("Trick XOR Treat").scale(0.8)
title.move_to(panel_bg.get_top() + DOWN * 0.4)
# Two circles for the Venn diagram
c1 = Circle(radius=1.4, color=BLACK).move_to(LEFT * 0.8)
c2 = Circle(radius=1.4, color=BLACK).move_to(RIGHT * 0.8)
# Create individual fill shapes for XOR (exclusive OR)
left_only = Difference(c1, c2).set_fill(ORANGE, opacity=0)
right_only = Difference(c2, c1).set_fill(ORANGE, opacity=0)
center = Intersection(c1, c2).set_fill(GREY, opacity=0) # XOR excludes center
# Create faces on left and right sections
face = VGroup()
# Left face
left_eye = Circle(radius=0.2, color=BLACK, fill_opacity=1)
left_eye.move_to(left_only.get_center() + UL * 0.3)
left_mouth = Parametric Function(
lambda t: [t, 0.15 * np.sin(t * PI * 5), 0], t_range=[-0.3, 0.3]
).set_stroke(color=BLACK, width=8)
left_mouth.move_to(left_only.get_center() + DOWN * 0.2)
# Right face
right_eye = Circle(radius=0.2, color=BLACK, fill_opacity=1)
right_eye.move_to(right_only.get_center() + UR * 0.3)
right_mouth = Parametric Function(
lambda t: [t, 0.15 * np.sin(t * PI * 5), 0], t_range=[-0.3, 0.3]
).set_stroke(color=BLACK, width=8)
right_mouth.move_to(right_only.get_center() + DOWN * 0.2)
face.add(left_eye, left_mouth, right_eye, right_mouth)
# Combine everything
panel = VGroup(
panel_bg,
left_only,
right_only,
center,
VGroup(c1, c2), # Circle outlines
title,
face,
)
panel.move_to(ORIGIN)
# Animate the panel appearing with smooth scaling
self.play(Fade In(panel_bg, scale=1.1), Write(title, run_time=1.2), run_time=1.0)
# Draw circles sequentially
self.play(Create(c1, run_time=1.0))
self.wait(0.5)
self.play(Create(c2, run_time=1.0))
self.wait(0.5)
# Dramatic filling animations with smooth rate functions
self.play(
left_only.animate.set_fill(ORANGE, opacity=1),
run_time=1.2,
rate_func=smooth,
)
self.wait(0.5)
self.play(
right_only.animate.set_fill(ORANGE, opacity=1),
run_time=1.2,
rate_func=smooth,
)
self.wait(0.5)
# Animate faces appearing on top of fills with bounce
self.play(Fade In(face, shift=UP * 0.3, scale=0.8), run_time=0.8)
# Final pause
self.wait(1.5)
The Manim API is refreshingly procedural—
a nice break from the declarative nature of SwiftUI, if you ask me.
You create objects, position them, and then explicitly tell them what to do.
The .animate
syntax provides some syntactic sugar for transformations,
but at its core, this is imperative programming at its finest.
Enter the Development Loop
Our example project includes a Mise task for quickly previewing our scene. It runs Manim at a low resolution (480p) and frame rate (15 fps), and then opens the generated file in QuickTime Player.
$ mise run preview
The project also has a render
task for when you’re ready to share it around:
$ mise run render
If everything works as intended, you should see something like this:
(Voiceover)
No 3blue1brown explainer video would be complete without the dulcet baritone of its creator. Alas, Mr. Sanderson had previous obligations that took him away from our costume party, so we’ll have to improvise. Let’s crank up the spook factor and have an AI… ghost? I guess? narrate our flick.
ElevenLabs has some scary-good text-to-speech models.
While manim-voiceover
provides integration,
it’s currently a bit temperamental with Python 3.13
and doesn’t support the latest
v3 models
for hyper-realistic voiceovers.
To get started, you import from manim_voiceover
(_fixed
)
and update the scene to inherit from Voiceover
.
from manim_voiceover_fixed import Voiceover Scene
from manim_voiceover_fixed.services.elevenlabs import Eleven Labs Service
class Trick XORTreat(Voiceover Scene):
def construct(self):
self.set_speech_service(
Eleven Labs Service(
voice_name="Liam",
model="eleven_v3",
transcription_model=None, # <-- workaround for https://github.com/Manim Community/manim-voiceover/issues/114
)
)
with self.voiceover(
text="""[annoyingly high-pitch nasal pedantic] Well, actually,
it's an exclusive or, also known as XOR..."""
):
# This wait() automatically waits until the voiceover finishes!
self.wait()
The wait()
inside the context manager waits until the voiceover finishes —
no manual timing required.
For recorded voiceovers especially,
this dramatically cuts down on how much time you spend in post
getting the timing right.
In fact, the finished video at the top was entirely rendered by Manim—
without any touch-ups in Final Cut or Premiere!
The first time you run this code,
Manim will helpfully (albeit unexpectedly) prompt you to enter an API token,
which you can get in your account dashboard.
That’ll get saved to an .env
file
and that should be the end of it.
The best explainer videos don’t just teach, they spark curiosity, they delight, they make complex ideas feel approachable. That’s what drew me to 3blue1brown in the first place, and why I’ve kept that silly meme in my Downloads folder all these years.
So if you’ve been sitting on a meme in your ~/Downloads
folder,
or just want an excuse to dust off those Python skills,
maybe this Halloween you dress up as a math YouTuber, too.
The costume’s open source, and honestly?
It’s a pretty good look.