Back to all posts

Building a diagram renderer with Svelte

December 7th, 2025

5 min read

Coding

Frontend

Web

I've been hearing about Svelte for a while now. People keep raving about how elegant it is, how it compiles away the framework overhead, how the DX is just chef's kiss. So naturally, I ignored it for way too long lol. But recently I finally decided to give it a proper go, and what better way to learn than by building something?

Enter Mermaider; a small web app I built to render Mermaid diagrams and export them as PNGs. Nothing groundbreaking tbh but I was tired of fighting with online mermaid renderers trying to squeeze every last dollar out of me, so I decided to just build my own. And it turned out to be a great playground to explore Svelte's patterns and Svelte 5's shiny new reactivity system!

Why Svelte?

Before diving into the code, let me quickly explain why Svelte caught my attention. Unlike React or Vue, Svelte is a compiler. It takes your components and compiles them into highly efficient vanilla JavaScript at build time. No virtual DOM, no runtime framework overhead. Your bundle ships just the code it needs. Which means smaller bundle sizes, faster runtime performance and less boilerplate code.

On top of that, with Svelte 5 they've introduced a completely new reactivity system based on "runes" that makes state management even more intuitive. Let's explore what I learned.

Svelte 5 Runes: The New Reactivity Primitives

The biggest thing I wanted to explore was Svelte 5's runes. If you're coming from React, think of them as Svelte's answer to hooks, but with a much cleaner syntax. Here's how I used them in Mermaider:

$state — Declaring Reactive State

In Svelte 5, you declare reactive state using the $state rune. It's beautifully simple:

<script lang="ts">
// state
let code = $state(`graph TD;
A-->B;
B-->C;
C-->A;`);
let errorMessage = $state('');
let scale = $state(1);
let panX = $state(0);
let panY = $state(0);
let isDragging = $state(false);
</script>

That's it. No useState hook, no ref() wrapper, no special syntax for updating. You just... use the variable. When you reassign it, Svelte knows to update the DOM. Coming from React where you need setX(newValue) for everything, this kinda felt too good to be true.

$derived — Computed Values

For values that depend on other state, Svelte 5 gives us $derived. In Mermaider, I used it to compute line numbers for the code editor:

<script lang="ts">
let code = $state(`graph TD;
A-->B;`);

// computed line numbers
let lineNumbers = $derived(() => {
const lines = code.replace(/\r/g, '').split('\n');
const count = Math.max(lines.length, 1);
return Array.from({ length: count }, (_, i) => i + 1);
});
</script>

Whenever code changes, Svelte automatically recalculates lineNumbers. No dependency arrays to manage, no useMemo to remember. Svelte figures out the dependencies for you.

$effect — Side Effects Done Right

The $effect rune is for running side effects when reactive values change. I used it to re-render the Mermaid diagram whenever the code changes:

<script lang="ts">
let mounted = $state(false);

// watch code changes (only after mount)
$effect(() => {
if (mounted && previewDiv) {
renderMermaid(code);
}
});
</script>

Again, no dependency array needed. Svelte tracks which reactive values are read inside the effect and re-runs it when any of them change. This is a huge DX improvement over React's useEffect where forgetting a dependency is a classic source of bugs.

Bindings: Two-Way Data Flow Made Easy

One of Svelte's killer features is its binding system. In React, you'd typically need to wire up an onChange handler to update state. In Svelte, you just... bind:

bind:value — Form Inputs

<textarea
bind:value={code}
class="flex-1 h-full font-mono border p-4"
></textarea>

That's it. The textarea's value is now synced with the code state variable. Type in the textarea, and code updates. Change code programmatically, and the textarea updates. No boilerplate required.

bind:this — Element References

Sometimes you need a reference to the actual DOM element. Svelte makes this trivial:

<script lang="ts">
let previewDiv: HTMLDivElement;
let editorTextarea: HTMLTextAreaElement;
</script>

<div bind:this={previewDiv} class="preview-container">
<!-- mermaid diagram renders here -->
</div>

<textarea bind:this={editorTextarea} bind:value={code}></textarea>

Now previewDiv and editorTextarea are references to the actual DOM elements. I used this to manipulate the preview container when rendering diagrams and to restore cursor position after inserting tabs.

Conditional Classes

Svelte has a neat shorthand for conditionally applying classes. In Mermaider, I used it to change the cursor style when dragging:

<div
class="cursor-grab"
class:cursor-grabbing={isDragging}
>
<!-- ... -->
</div>

When isDragging is true, the cursor-grabbing class is added. When it's false, it's removed. No ternary operators, no classnames library needed.

Final Thoughts

Aaand that's it! Building Mermaider was a great way to get hands-on with Svelte 5. I'm really enjoying the DX of the runes system, feels like it takes the best ideas from reactive programming and makes them feel natural and unobtrusive. No more juggling dependency arrays or remembering to wrap things in special functions. Everything just feels natural and intuitive.

If you're curious about Svelte, I'd encourage you to just build something small with it. The learning curve is gentle, and the DX is genuinely incredible. The official tutorial is also excellent if you want a more structured introduction.

Oh, and if you want to check out Mermaider or use it to render your own Mermaid diagrams, here's the repo! It's nothing fancy at all but it does the job and it was a fun learning project.

Happy coding!

— Nathan

←  Back to all posts