~/satyajit

Shaders goes MIT: the components are the menu, the compiler is the meal

mdjsonmcp

2026-10-06 · 20 min · webgpu · gpu · typescript · compilers · licensing · mcp · developer-tools · creative-tools · privacy

Why read this

Notabletop 60%

Reads the Shaders engine to show how a layer tree compiles into GPU passes, what still triggers a recompile, and what the MIT switch does and does not open.

  • Runs in a browser
  • Original analysis
  • Widely used

Developer tools & infraMITPractitioner tool

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
3 of 3: Open, permissive, runs on reader hardware with instructions
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
2 of 3: A widely used model, tool or lab release
Only here?
2 of 3: A teardown or measurement few others did

Score 64 of 100, ranked 168 of 445 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

On 6 October the account behind the shaders npm package posted "Shaders is now open source 🔥", with 200+ WebGPU components, code export "for any framework, for free", and a follow-up aimed at people like me: "Today's coding agents can write shaders, but the output is still raw WGSL code that may not perform well when deployed in real-world projects." The announcement says the rendering engine, every component and the framework bindings are now MIT.

I went in expecting a gallery of fragment shaders with a React wrapper on each. Two things changed my mind. The first is the licence history: this was not an obscure repo becoming public, it was a package with 1,143,720 downloads in the 30 days to 4 October whose terms, until the day before the post, said commercial use needed a paid plan. The second is the engine. The 199 effects are the menu. The thing underneath them is a small compiler that decides how many GPU passes your layer stack costs, and it is the most interesting code in the repository.

shader-effects-inc/shaders@935f71a · snapshot 2026-10-07
tracked files
1,207
license
MIT
branch
main
tests
487 files
source
7.1 MB
commit date
2026-10-06
source by language
TypeScript7.0 MB(692)Vue74.0 kB(4)Svelte59.1 kB(5)JavaScript10.9 kB(5)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

Read at 935f71a (6 October 2026), a single public commit. Engine in packages/core; the framework packages are generated from it.

local clone, 2026-10-07 at 935f71a — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

The shaders.com landing page: a purple and blue blurred gradient behind the headline 'The design platform for shader magic', framework logos for Vue, React, Svelte, Solid, Framer and JavaScript, GitHub and Design for free buttons, and the design editor below with a layer list on the left and a colour-stops panel on the right.
The landing page and, under it, the design editor: layers on the left, the selected component's props on the right. The editor is not part of the open-source release (shaders.com, the README's og image).

What was actually opened

The npm registry tells the licence story better than the announcement. The package has 271 published versions. The very first, 0.0.1, says ISC. The next 269, from 2.0.0-alpha.0 through 3.2.475 on 29 September, carry no license field at all. Only 4.0.0, published at 02:55 UTC on 6 October, says MIT. I pulled the 3.2.475 tarball, and its README is blunt:

Free for personal, non-commercial, and evaluation use … Commercial use requires an active Pro or Team license.

Its LICENSE file was the "Shader Effects License Agreement (v1.5)", which called the engine "Proprietary Technology" and forbade "circumventing license validation" and reverse engineering. The 4.0.0 tarball's LICENSE is the plain MIT text, and the repository at 935f71a has one LICENSE file, the same one. So anyone who shipped a Shaders background on a client site last month was out of licence, and as of this release they are not. To me this is the real news in the post, and the announcement half-says it when it offers to "sort it out" for anyone who bought Pro in the last 30 days only for commercial use or export.

What MIT covers, by the new licence page (v2.0, effective 6 October): the engine, every component, the five framework bindings and the CLI. What it does not cover: the design editor, the 1,000+ Pro presets, the 55+ sections, and the MCP server, which is a hosted service at https://shaders.com/mcp. Presets fall under a separate content licence that forbids redistributing them as presets, templates or component libraries. The split is reasonable. It does mean the "agents" pitch leans on a service that is not in the repository, which I come back to below.

One loose end: the agent-facing llms.txt still describes the old deal the day after. It lists code export and the MCP server under "Shaders Pro — A paid subscription", while the README says the MCP works "on any account, free included" and the licence says export is "available to every account at no charge". If you point an agent at the docs, it will read the stale version.

A component is a plain object

Every effect lives in packages/core/src/shaders/<Name>/index.ts next to a cover.jpg. There are 199 of them in 26,260 lines; the site's own navigation says "190+" and the post says "200+", and the llms.txt count, 199, is the one that matches the folder. Here is the whole of LinearGradient's drawing logic (packages/core/src/shaders/LinearGradient/index.ts:101-105):

paint: rampOver(
    dist.linear({from: p('start'), to: p('end'), angle: p('angle')}),
    stops(p('colorSpace')),
    {edges: p('edges')},
)

Above it sit eight props, each with a default, a transform that says what kind of value it is (colour, position, angle) and ui metadata for the editor's controls. There is no WGSL in the file. rampOver, dist.linear and stops are words from a standard library (packages/core/src/std), and the engine compiles the composition. Two of the props carry compileTime: true, edges and colorSpace. That flag matters later.

The role field sorts the 199 into a handful of kinds, and the kind decides what the compiler can do with a component. Counting the role: lines in every file: 50 generators (gradients, noise, light), 66 filters, 18 warps (Twirl, Bulge, Liquify), 23 shape effects such as Glass and Neon that need a signed distance field, 15 plain shapes, 19 simulations (fluids, particles, reaction-diffusion), 5 media sources (image, video, webcam, text, HTML) and a few structural ones like Group. A filter additionally declares a species: pointwise if it only needs the colour at the current pixel, like Invert, Tint or Duotone, and gather or custom if it reads neighbours, like Pixelate or Blur.

The lowest layer is not hand-written WGSL either. The kit functions are TypeScript marked 'use gpu', which TypeGPU compiles to WGSL. I counted 1,148 of them across 204 files. The normal blend mode is four lines (packages/core/src/gpu/kit/blend.ts:58-62):

export const normal = tgpu.fn([d.vec4f, d.vec4f, d.f32], d.vec4f)((base, overlay, opacity) => {
    'use gpu'
    const overlayAlpha = overlay.w * opacity
    return overComposite(base, overlay.xyz, overlayAlpha)
})

Writing shader code as TypeScript buys something concrete here. The header of blend.ts notes these functions are "CPU-executable", and the test suite runs them on the CPU against golden values. The core has 267 test files, and there are 1,713 it( or test( calls in them. Iwo Plaza, who leads TypeGPU, was among the first to reply to the launch post. This is the biggest TypeGPU codebase I have read.

If you want to write your own component, defineShader takes the same shape, and the escape hatch is a wgsl template string whose free identifiers are bound for you. std/wgsl.ts scans the body for names: any prop you mention becomes a parameter, as do uv, time, aspect, viewport, pointer and, in a filter, child. Mention childTexture and the body is reclassified as a gather filter, which changes how it is compiled. The README's example:

export const Halo = defineShader({
  name: 'Halo',
  props: {
    color: { default: '#ffd166', transform: transformColor },
    center: { default: { x: 0.5, y: 0.5 }, transform: transformPosition },
    radius: { default: 0.6 },
  },
  paint: wgsl`
    let d = length((uv - center) * vec2f(aspect, 1.0)) / radius;
    return vec4f(color.rgb, 1.0 - smoothstep(0.8, 1.0, d));
  `,
})
A dark grid of rounded tags, each showing a small preview dot and a component name in JSX form, such as FlowField, FlowingGradient, FlutedGlass, LensDistortion, LensFlare, MultiPointGradient, Nebula, Neon, Polygon, Posterize, Prism, Shatter, SimplexNoise, SineWave, TiltShift, TimeTrail, Tint, Voronoi, Voxels, Water, Watercolor, CarbonFiber, AngularBlur and Arc.
Part of the component list as JSX tags, a frame from the launch video at about nine seconds (Shaders' launch video on X).

The layer tree is compiled, not drawn

In your framework you write a tree. <Shader> is the canvas; its children are layers, composited in order; a filter can wrap children or apply to the siblings before it. The naive way to render that is one pass per layer, each drawing into a texture the next one reads. On a 1440 by 900 hero at the desktop pixel-ratio cap of 2, every such pass is 5,184,000 fragment invocations plus a full-frame texture write and read.

The composer (packages/core/src/gpu/composer.ts, 1,738 lines) does something better. It walks the tree and builds an expression for each pixel, emitting WGSL only when the tree's shape is settled, and it starts a new pass only where it must. The test snapshots show what comes out. Two generators with a multiply blend compile to a single line in a single fragment function (packages/core/src/__tests__/gpu/__snapshots__/composer.test.ts.snap:135):

var composed_0 = blend_multiply(genBody(uniforms.n_g1.speed, uv), genBody(uniforms.n_g2.speed, uv), uniforms.n_g2._opacity);

No intermediate texture. Both generators are evaluated at uv and blended in registers. The sibling loop, composeSiblings, applies four rules, and once you know them you can predict the cost of any stack.

A generator is evaluated inline and blended over whatever has been composed so far. A pointwise filter replaces the composed colour with f(composed), still inline; the code calls this the replace path (composer.ts:1080), and it is why stacking a Tint and a Vignette on top of a gradient costs arithmetic and nothing else. A filter that reads neighbours cannot work on an expression, because it needs the colour at other pixels. So at composer.ts:1059 the composer wraps everything below the filter in a blend over transparent black and hands it to convertToTexture, which registers an RTT boundary: one extra pass that renders the content below into an rgba16float texture, which the filter then samples. The snapshot for a generator under such a filter shows exactly that, a rtt_rtt_0 pass that returns the generator, and a final pass that reads textureSample(rtt_0, linearClamp, uv).

The fourth rule is the clever one, and it handles distortions. A Twirl moves pixels: the colour at uv is the colour of the content at twirlUV(uv). Here is that map (packages/core/src/gpu/kit/warpMaps.ts:186-199, abridged):

export const twirlUV = tgpu.fn([d.vec2f, d.f32, d.vec2f, d.f32], d.vec2f)((center, intensity, uv, aspect) => {
    'use gpu'
    const centerPos = d.vec2f(center.x, 1.0 - center.y)
    const delta = uv.sub(centerPos)
    const acd = d.vec2f(delta.x * aspect, delta.y)
    const angle = intensity * std.length(acd)
    // rotate acd by angle, undo the aspect correction, add the centre back
})

If the content under the twirl is a texture, you have to render it first and resample it, which softens it and costs a pass. But if the content is generators, you can just evaluate them at the moved coordinate. The comment at composer.ts:1173 states the identity: "A UV remap commutes with pointwise compositing: blend(cᵢ)(f(uv)) is the same image as blend(cᵢ(f(uv)))." So a Twirl wrapped around a gradient and a noise layer compiles to gradient(twirlUV(uv)) blended with noise(twirlUV(uv)), with no texture, and text or an image inside it samples its own source pixels at full resolution instead of a blurry intermediate. analyticPushdown.test.ts pins this with rttPasses === 0. Anything that is not pointwise in screen space (a mask source, a layer transform, a nested filter that resamples) disqualifies the subtree and falls back to the texture path, and the push-down stops at 24 nodes so a pathological tree cannot emit an enormous fold.

When the warp is a sibling sitting on top of composed content rather than wrapping its children, it cannot push down, but a run of consecutive warps still folds into one UV over a single texture. Two twirls in a row cost one RTT pass, not two.

Here is that sibling loop, re-implemented over a stack you can edit. It emits the shape of the WGSL the composer would, pass by pass, and counts what each frame costs.

stack, first child at the top
  1. LinearGradient
  2. SimplexNoise
  3. Blur
  4. Tint
pass 1: render to rtt_0 (rgba16float)
return blend_normal(vec4f(0.0), blend_normal(linearGradient(uv), simplexNoise(uv), 1.0), 1.0);
compute: blur H: rtt_0 → tmp_rtt_0 (1024 x 640, 49 taps)
compute: blur V: tmp_rtt_0 → blur_rtt_0 (1024 x 640, 49 taps)
pass 2: final, to the canvas
var composed_0 = tint(vec4f(textureSample(blur_rtt_0, linearClamp, uv).rgb, textureSample(rtt_0, linearClamp, uv).a));
return vec4f(linearToSrgb(tone_linear(composed_0.rgb)), composed_0.a);
render passes per frame2compute dispatches per frame2fragment invocations, 1440 x 900 hero at DPR 210,368,000compute pixels (fixed, any canvas size)1,310,720intermediate textures, full-frame1 (41.5 MB)

Try "blur in the middle": the gradient and noise get rendered to rtt_0, Blur's two compute passes run over that, and the Tint above the blur is applied inline in the final pass. Move the Tint below the Blur and it gets folded into the texture pass instead, with the same pass count. Swap Blur for Pixelate and the compute passes disappear, though the texture stays. Now try "twirl nested": three layers, one render pass, no textures.

Blur deserves a note because it is the effect people reach for first. gaussianBlur in std/effects/blurs.ts does not blur in the fragment shader. It renders the child to a texture, then runs a separable Gaussian as two compute passes at a fixed 1024 by 640 (gpu/kit/blur.ts:57-58), 49 taps each way with the default half-kernel of 24, and the final pass bilinear-samples the result. Its cost does not grow with the canvas, which is the right call on a 4K monitor. The doc comment states the trade-off: only colour blurs, "the child's alpha stays sharp".

The practical rule I took from this: pointwise effects are close to free to stack, every gather filter costs a full-frame texture (41.5 MB at 2880 by 1800 in rgba16float), and if you want a distortion over several layers, nest the layers inside it rather than putting it after them.

Props are uniforms, until they are compile-time

The second thing the engine gets right is what it does when a prop changes. Every prop of every node lives in one packed uniform buffer: a struct of per-node structs plus a _sys struct with time, viewport, aspect and pointer. The snapshot shows it, n_g1, n_g2 and n_root each padded to 16 bytes, bound once at @group(0) @binding(0). Changing speed or a colour writes a FieldHandle, the writes coalesce, and one buffer.patch goes out per frame (gpu/uniformStore.ts, header). No shader is rebuilt. That is what makes dragging a slider in the editor, or animating a prop from React state, cheap.

Some props cannot be uniforms because they change the code itself. edges on LinearGradient picks between stretch, transparent, mirror and wrap branches; colorSpace picks the interpolation functions. Those are marked compileTime, and they go into a structural hash along with each node's id, parent, component name, blend mode, mask, order, visibility, whether opacity is zero, transform, box and requiresRTT (composer.ts, collectStructuralHashInputs). The hash is a 32-bit FNV-1a over the joined strings with the length appended (gpu/pipelineCache.ts). A new hash builds a new composition, but the old one keeps rendering until the new one has drawn its first frame, the "swap-when-ready" pattern, so a recompile never flashes. An LRU keeps four built compositions (pipelineCache.ts:79), because people toggle back and forth while editing.

Opacity shows how carefully the boundaries were drawn. A first child at exactly opacity 1 is emitted with no blend at all, so its _opacity uniform is never read. The base hash only knows "zero or not", so the renderer adds a second input, whether opacity is below 1, and a comment in gpu/index.ts (around line 1373) explains why: without it "the slider would do nothing". So dragging a layer from 1 to 0.9 recompiles once, and dragging it from 0.9 to 0.3 is a uniform write.

The Shaders design editor: a canvas showing a blue and violet composition with a pink and yellow block of repeated digits, a selection box around one layer, and on the right a property panel with groups for palette, effect, animation and appearance.
The design editor with a layer selected and its props on the right. Every control there maps to a uniform write unless the prop is compile-time (Shaders' launch video on X, about 19 seconds in).

What it does every frame

The frame loop is simple, and I think it leaves performance on the table. frameGate in gpu/frame.ts:202 has two rules: off-screen (an IntersectionObserver says the canvas is not visible) it draws once a second, and on-screen it draws up to 60 times a second. Nothing in the gate asks whether anything changed. A static gradient with no animated prop keeps redrawing at 60 Hz while it is in view. For one hero that is cheap; for a page of ten small cards it is ten canvases at full rate, and the only lever is a per-renderer setFrameRateCap.

The device-class code is more careful. maxPixelRatioForDevice in utilities/device.ts:33-35 caps the backing store at a pixel ratio of 2 on desktop and 1.5 on a touch device with a viewport under 1,366 CSS pixels. The comment works out why: fragment cost scales with the square of the ratio, so 1.5 instead of 2 is about 44% fewer invocations. Several components read the same signal once at build time and compile a lighter mobile variant with fewer texture taps.

It is WebGPU only. The renderer's header says "There are no WebGL paths", and gpu/support.ts explains the failure policy: on a browser that cannot get a working device, "the ONLY acceptable outcome is a transparent canvas and a silent console". That is the right policy for a decorative background, and it means you must design the page to look fine without the effect: a browser without WebGPU, or a blocklisted driver, sees nothing where the effect should be. The workspace package.json still lists webgl and glsl as keywords, and the telemetry type still has a 'webgl' renderer value, leftovers from an older engine.

Code export is a tree printer

"Export code for any framework" sounded to me like it might mean exporting the shader. It does not. The editor's document is preset JSON, a tree of {type, props, children}, the same format createShader in the JavaScript binding takes. Each framework package has a generatePresetCode.template.ts that prints that tree as JSX, a Vue template or Svelte markup, sorting props, dropping ones at their defaults and dropping a full-frame bounding box. The exported file imports from shaders/react or its siblings. The WGSL is generated at runtime in the browser, by the composer, every time.

I have no complaint about it; it is why one engine serves five frameworks. The component wrappers themselves are generated too: core/scripts/generate-components.ts stamps one template per framework for each of the 199 folders, substituting the name and which props accept a driver. The CLI's npx shaders install fetches a component file from /api/plugin/shaders/<id>/code on shaders.com for a design in your account and writes it into your components folder with a lock file. The CLI is MIT; the endpoint is the platform.

The agent pitch, checked

The launch thread's argument is that agents write raw WGSL that "may not perform well", and should build on tested components instead. After reading the composer I half agree, for a reason the thread does not give. An agent that writes <Blur> gets a separable compute blur at a fixed resolution and a pass plan it did not have to think about. An agent writing its own WGSL gets whatever it wrote, and the pass structure is the part agents get wrong, because it is a property of the whole tree and not of any one shader.

What in the repository is actually for agents? The docs. packages/core/scripts/docsManifest.ts builds one manifest of every word in the std library from its TypeScript signatures and doc comments, and that manifest generates the primitive reference, the per-category markdown and llms-full.txt (249,110 bytes when I fetched it). A test, docsManifest.test.ts, ratchets the count of undocumented exports and the baseline is 0. I wish more libraries did this.

The MCP server is the other half and it is not here. npx shaders install-mcp writes an HTTP server entry pointing at https://shaders.com/mcp into Claude Code, Cursor, Codex and others (packages/shaders/src/cli/installMcp.ts), and the agent signs in to a Shaders account on first connect. Its tools browse your saved designs, search the Pro preset library and generate signed distance fields from SVGs, by the MCP guide. Useful, but a hosted service with an account, not the open-source part.

I could not check "production-tested" or "real performance" beyond the code. There is no benchmark in the repository and no published frame-time numbers. The telemetry below suggests the company has those numbers; they are not public.

The telemetry you get by default

Each framework's <Shader> component starts a collector once the canvas is rendering (packages/react/src/engine/Shader.tsx:159-187). It runs only on hostnames that are not shaders.com or localhost (telemetry/index.ts:47), skips browsers with Do Not Track set (telemetry/index.ts:13), and then samples: Math.random() < 0.05 (telemetry/config.ts:2). A sampled page collects for 5 seconds, the first half second of it a warm-up, and posts to https://shaders.com/api/telemetry the page's hostname, browser family, device type, frame-rate statistics, and the names of the components on it. Preview components skip the dice and always report.

None of this is hidden. The licence page's section 6 describes it, gives the same opt-out, and says the data contains no personal information. The README does not mention it, and I think it should, because a library under MIT that phones home from 5% of its users' visitors' browsers is something you want to know before it lands on a client site. Pass disableTelemetry (or the same option to createShader) if your privacy policy has an opinion. I checked the published 4.0.0 tarball as well: the endpoint and the 0.05 rate are in its bundles.

Twelve square thumbnails in two rows: a blue-to-green linear gradient, a pink and violet mesh gradient, a green aurora on black, grey simplex noise, a glass disc over a checkerboard, a liquid chrome disc, a twirled coastal photo, a soft watercolour blob, a halftone circle, a dithered circle, magenta Voronoi cells and blue reaction-diffusion stripes.
Twelve of the 199 components, as their own cover images. Top row: LinearGradient, MeshGradient, Aurora, SimplexNoise, Glass, LiquidMetal. Bottom row: Twirl, Watercolor, Halftone, Dither, Voronoi, ReactionDiffusion. I arranged the grid; the images are the repository's cover.jpg files (shader-effects-inc/shaders, packages/core/src/shaders).

Would I use it

For a marketing page or a product hero, yes, now that it is MIT. The components are well past toy quality, and the composer means a reasonable stack compiles to one or two passes without you thinking about it. Pass disableTelemetry, design the fallback for browsers without WebGPU, and keep an eye on how many canvases you put on one page, since each one redraws at 60 Hz while visible.

For learning, read composer.ts and the analyticPushdown tests before the components. The push-down is a small idea, that a coordinate remap commutes with pointwise blending, applied with care about where it stops being true, and it is the kind of thing a hand-written shader stack never gets. If you came here from GTA V's WebGPU port or Voxel Musou's post chain, this is the opposite end of the same API: no translated engine, just a library that treats a layer tree as a program.

For agents, the open part is the vocabulary and the compiler, and those are the parts that make an agent's output better. The catalogue of designs to start from, and the MCP service that serves it, are still the product. That is a fair trade, as long as nobody mistakes one for the other.

How I checked

I shallow-cloned shader-effects-inc/shaders at 935f71a and read packages/core/src/gpu (composer, uniform store, pipeline cache, pass manager, frame loop, support), the std lowering and wgsl binding, the blur kit, the telemetry module, the five framework Shader components, the code generator and the CLI. Counts come from the tree: component folders, role: lines, 'use gpu' markers, test files and it(/test( calls. The emitted WGSL quoted is from the repository's own composer snapshots; I did not run the engine, so the pass counts in the widget follow the composer's rules as written rather than a GPU trace, and the 1440 by 900 frame is an example size, not a measurement. The licence history comes from the npm registry's version list and from unpacking the 3.2.475 and 4.0.0 tarballs; download counts are npm's API for 5 September to 4 October. The launch thread and its replies were read through the fxtwitter mirror, and the announcement, licence page, MCP guide and llms.txt from shaders.com on 7 October. I deleted the clone afterwards.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Shaders goes MIT: the components are the menu, the compiler is the meal", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026shaderswebgpu,
  author = {Satyajit Ghana},
  title  = {Shaders goes MIT: the components are the menu, the compiler is the meal},
  url    = {https://ai.thesatyajit.com/articles/shaders-webgpu},
  year   = {2026}
}
share