How to install and use @craftedstudios/shaders: avatars, cursors, outlines, still images and performance.
Live shaders for product UI: avatars from any name, email or user ID, presence cursors with the avatar built in, and outlines for your own buttons and cards. Everything on a page shares one WebGL context. There's a PNG export for places a shader can't run, and a React entry.
See every effect, and try your own seeds, at shaders.craftedstudios.co.
npm i @craftedstudios/shaders
No dependencies. React is optional.
Give each avatar a seed. The seed decides the effect, the colors and the layout, so the same person always gets the same avatar.
React:
import { Avatar } from '@craftedstudios/shaders/react'; <Avatar seed={user.id} size={40} />
Web component:
import { defineAvatarElement } from '@craftedstudios/shaders'; defineAvatarElement();
<crafted-avatar seed="ada@northwind.co" size="40"></crafted-avatar>
Plain JavaScript:
import { avatar } from '@craftedstudios/shaders'; const a = avatar(el, { seed: 'ada@northwind.co', size: 40 }); a.update({ effect: 'thermal' }); a.destroy();
Every option except seed is optional. The same names work as React props, web component attributes (ring-color for ringColor) and plain JavaScript options.
seed
ring-color
ringColor
size
32
effect
auto
linear
radial
thermal
topography
plasma
colors
motion
still
interaction
goo
follow
repel
distort
pulse
orbit
turbulence
none
speed
1
shape
circle
squircle
square
ring
0
label
grain, distortion, scale, stretch and thickness fine-tune the look. The Lab shows what each one does.
grain
distortion
scale
stretch
thickness
The same seed gives the same avatar everywhere: on the server, in the browser and in a PNG. Nothing is uploaded, fetched or stored.
Use something stable. An email works until someone changes it; a user ID never does.
For email, native apps and social images, export a PNG. It takes the same options and comes out up to 2048px square.
import { toBlob, toDataURL } from '@craftedstudios/shaders'; const png = await toBlob('ada@northwind.co', { size: 1024 }); const src = await toDataURL('ada@northwind.co', { effect: 'radial' });
For multiplayer, show where people are with their avatar built in. There are two styles: arrow puts the avatar beside the name, and avatar makes the avatar itself the pointer. Your app already knows where each cursor is, so pass x and y and the tip lands there, inside the nearest positioned element.
arrow
avatar
x
y
import { Cursor } from '@craftedstudios/shaders/react'; <Cursor seed={user.id} name={user.name} variant="avatar" x={x} y={y} />
The seed decides how the cursor looks. name is the text on its tag: pass whatever your app shows for that person. Leave it out and the cursor gets an anonymous name from its seed, like Swift Otter. The same seed always gets the same name, so for visitors who aren't signed in, a session ID gives each one their own. Set name to false for no tag at all.
name
false
nameFor(seed) returns the same anonymous name, for showing it elsewhere, like a list of who's here.
nameFor(seed)
Without React, cursor() returns a cursor you move as positions arrive. move() is cheap enough to call on every update.
cursor()
move()
import { cursor } from '@craftedstudios/shaders'; const c = cursor(canvas, { seed: user.id, name: user.name, variant: 'arrow' }); c.move(x, y);
zoom draws a cursor larger, with its avatars still sharp.
zoom
The same effects, drawn as a thin outline around something of yours: a button, a card, a field. A light travels the edge and comes to the pointer when it is near, with a faint glow where it passes. Your element is left as it is; the outline is laid over it and lets clicks through.
import { Outline } from '@craftedstudios/shaders/react'; <Outline seed="checkout"> <button>Get started</button> </Outline>
Wrap one element. The outline measures it, so it follows the element's size and its rounded corners. If your element moves or scales on press, put that on the Outline instead, so the two move together.
Outline
glow
0.5
light
both
pointer
radius
Six metals are for outlines only, and a seed never picks them: ask for one by name. Each keeps its own colors unless you pass yours, and its highlights bloom a little past the edge.
chrome
liquid-metal
gold
rose-gold
titanium
iridescent
Without React, outline() takes an element that is already on the page. For one that can't hold anything inside it, like an input, wrap it and outline the wrapper.
outline()
import { outline } from '@craftedstudios/shaders'; const o = outline(button, { seed: 'checkout' }); o.update({ effect: 'gold', thickness: 2 }); o.destroy();
When someone has asked their device for reduced motion, every avatar holds still and stops reacting to the pointer, and an outline's light stops where it is. You don't need to do anything.
To freeze avatars yourself, set motion to still, and interaction to none to stop the hover as well. A still avatar draws once and costs nothing after that.
<Avatar seed={user.id} motion="still" interaction="none" />
Every avatar, cursor and outline on a page shares one WebGL context, however many there are. Only the ones on screen draw, and everything pauses while the tab is hidden. Without WebGL, each avatar shows a gradient in its own colors instead.
The React component renders its gradient on the server, so the page loads with the avatar already in place and nothing shifts when the shader takes over. It works with Next.js and other frameworks that render on the server.