Mesh gradients are everywhere right now. From Stripe to Linear to Apple — that soft, fluid, almost liquid blend of colors became the modern alternative to flat gradients and stock illustrations. I wanted that look for my own projects, but without exporting static images or pulling in a heavy WebGL shader.
So I built Svesh — github.com/koodeau/svesh — a tiny Svelte single-component library that generates mesh gradients on the fly.
This is the story of how it works and what I learned building it.
Why mesh gradients are hard
A regular CSS gradient is linear. You have a direction and color stops, and the browser interpolates between them. It’s predictable, but it looks… predictable.
A mesh gradient is different. You have a 2D grid of control points, each with its own color and position. Colors blend in multiple directions at once, creating organic shapes that feel natural. You can’t do that with linear-gradient or radial-gradient alone.
Most solutions out there fall into two camps:
- Image-based: Generate it in Figma and export a PNG. Not responsive, not dynamic, heavy.
- Shader-based: Use WebGL/fragment shaders. Beautiful, but overkill for a background and hard to drop into a Svelte app.
I wanted a middle ground: native, lightweight, canvas-based, and dead simple to use in Svelte.
Introducing Svesh
Svesh is built on a simple idea: generate a randomized mesh on a <canvas> element using a set of colors.
The entire API is this:
<script>
import { Svesh } from "svesh";
</script>
<Svesh />That’s it. One component, no configuration required. Each time it mounts, it generates a unique gradient.
Under the hood, it renders to canvas, not SVG filters. That keeps it fast, resolution-independent, and easy to layer behind content with position: absolute.
You can install it in any Svelte / SvelteKit project:
npm i --save-dev sveshHow it works under the hood
The core of Svesh is a mesh generator. It doesn’t use an external noise library — it uses deterministic randomization based on a meshId.
Here’s the mental model:
- A meshId defines the shape. If you don’t pass one, Svesh picks a random number. That number seeds the positions of the mesh points.
Same
meshId= same shape every time. DifferentmeshId= new organic shape. - Colors define the palette. By default, Svesh picks random vibrant colors. But you can pass your own brand palette and it will distribute them across the mesh.
- Canvas does the blending. Instead of building complex CSS with dozens of radial gradients (like meshgrad does), Svesh draws bezier surfaces on canvas. This gives us smooth interpolation without banding and lets us control width/height precisely.
The component structure is intentionally small:
// Simplified internal flow
// Svesh.svelte
onMount(() => {
const id = meshId ?? Math.floor(Math.random() * 10000);
generateMesh(canvas, {
colors: colors ?? randomColors(),
id,
width,
height
});
});This approach makes it incredibly cheap to render. No WebGL context, no dependencies, just Canvas 2D API.
Designing an API people actually want to use
When I build open-source tools, I think about two users: the one who wants it to “just work” and the one who wants full control.
For the first user, <Svesh /> is enough.
For the second, I exposed props that cover 95% of real use cases without overcomplicating the component:
<script>
import { Svesh } from "svesh";
// Your brand colors
const colors = ["#ef008f", "#6ec3f4", "#7038ff", "#ffba27"];
// Lock the gradient so it doesn't change on reload
const meshId = 666;
</script>
<Svesh
{colors}
{meshId}
fixMeshId={true}
width={1920}
height={600}
instances={1}
identifier="hero-background"
/>Here’s what each prop does:
colors: Array of hex colors. Default is random vibrant.groupColors: When true, all instances share the same color scheme. Great for consistent sections.instances: Need 5 different gradients for a list of cards? Set instances=5 and Svesh will generate that many canvases with unique IDs.prefix: The prefix for canvas IDs. Useful to avoid collisions.width/height: Exact canvas size. Defaults to 1920x600, perfect for hero sections.meshId+fixMeshId: For static, reproducible gradients. Ideal for production where you don’t want the background to change on every refresh.identifier: Important if you use Svesh more than once on the same page. Change it to keep DOM IDs unique.
This API came from real usage. I first used Svesh in my side projects, but now on koodeau.com itself, and every prop was added because I actually needed it.
Why Svelte was the perfect fit for this
I could have built this as a vanilla JS library. But Svelte makes component libraries like this almost trivial.
- No wrapper hell. The component is the canvas. No useEffect, no useRef cleanup dance. onMount creates, onDestroy cleans.
- Reactivity for free. If a user changes colors prop, Svelte will re-run the generation. I don’t need to write a diffing logic.
- Zero runtime cost. Svesh compiles to tiny vanilla JS. The whole library is less than a few KB gzipped. No virtual DOM overhead for something that just needs to paint once.
If I were rewriting it today for Svelte 5, I would use runes — which I covered in Getting started with Svelte 5 runes — to make shared state even cleaner:
<script>
let { colors = randomColors() } = $props();
let meshId = $state(() => Math.floor(Math.random() * 10000));
let canvas = $derived.by(() => generate(colors, meshId));
</script>The current version still uses Svelte 4 syntax for maximum compatibility, but a runes-based v2 is on the roadmap.
Lessons from shipping a tiny open source library
Building Svesh taught me a few things I also touched on in Docker for frontend developers — tooling should reduce friction, not add it.
- Small is a feature. I was tempted to add animation, noise controls, export to PNG. I didn’t. The best open-source components do one thing well.
- Documentation is the product. The README with one example
<Svesh />got more stars than any advanced feature. If people can’t use it in 30 seconds, they won’t use it. - Build for yourself first. I built Svesh because I needed it for my projects and this website. That meant the API was battle-tested before the first npm publish.
What’s next and how to contribute
Svesh is working, lightweight, and production-ready. But there are a few things I’m exploring:
- Svelte 5 runes rewrite with
$propsand$derived - Optional animated mode with subtle morphing
- Export hook to get canvas as data URL
If you use SvelteKit, give it a try. It takes literally 10 seconds to get a beautiful hero background that isn’t another boring linear gradient.
Repo: github.com/koodeau/svesh
If you build something cool with it, tag me — I would love to see it. And if you have ideas, PRs are welcome. Please star the repo if you like it ;)
Built with Svelte, TypeScript, and a lot of random color experiments.
