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

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/shaders-webgpu
> date: 2026-10-06
> tags: webgpu, gpu, typescript, compilers, licensing, mcp, developer-tools, creative-tools, privacy

On 6 October the account behind the `shaders` npm package
[posted](https://x.com/npm_i_shaders/status/2107467668009963819) "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](https://shaders.com/updates/shaders-is-open-source) 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.

<RepoCard repo="shader-effects-inc/shaders" note="Read at 935f71a (6 October 2026), a single public commit. Engine in packages/core; the framework packages are generated from it." />

<Figure
  src="https://ai.thesatyajit.com/articles/shaders-webgpu/fig1.jpg"
  alt="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."
  caption="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](https://shaders.com/license) (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`](https://shaders.com/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`):

```ts
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](/articles/pmndrs-math-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`):

```ts
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:

```ts
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));
  `,
})
```

<Figure
  src="https://ai.thesatyajit.com/articles/shaders-webgpu/fig2.jpg"
  alt="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."
  caption="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`):

```wgsl
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):

```ts
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.

<PassPlanner />

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.

<Figure
  src="https://ai.thesatyajit.com/articles/shaders-webgpu/fig3.jpg"
  alt="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."
  caption="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.

<Figure
  src="https://ai.thesatyajit.com/articles/shaders-webgpu/fig4.jpg"
  alt="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."
  caption="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](/articles/gta5-in-the-browser) or
[Voxel Musou's post chain](/articles/voxel-musou), 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.
