Shaders
Shaders / Component

Component <fk-shader>

Seven styles from the Lab, packed into one small web component for your own pages. It renders an image or video as a live field of dots, squares, lines, dither, ASCII or LCD segments, or as soft glow. It reads your , and you can push it around with your finger or cursor.

11 KB gzipped · no dependencies · web component · TypeScript types

Installation

Two ways in. Pick one.

Option A: link it

One line, like loading a web font. This address is version 2.2.0 and never changes, so browsers keep it for a year and the integrity hash proves it is the file you expect.

  1. Add the script once, anywhere on the page.
    <script src="https://shaders.fedekotek.design/v/2.2.0/fk-shader.min.js"
      integrity="sha384-v9OVsPd28wQWZ35YMxTUkP6H31rE7VTpesVZ+rXCmYRXj5PnzNCa8dsFiMU+MAtT"
      crossorigin="anonymous" defer></script>

    Want every fix as it ships instead? This address always serves the latest build, including major versions that may change the API. Pin for production.

    <script src="https://shaders.fedekotek.design/fk-shader.min.js" defer></script>
  2. Give it a size in your CSS. It fills whatever box you put it in.
    <style>
      fk-shader { display: block; width: 100%; aspect-ratio: 3 / 4; }
    </style>
  3. Drop the tag where you want the picture, as in Usage.

Option B: copy it

Own the code, the way shadcn works. Nothing to install, nothing to keep in sync.

  1. Download fk-shader.js (readable) or fk-shader.min.js (about 11 KB gzipped) into your project. Using TypeScript? Take fk-shader.d.ts too.
  2. Load it, with the path relative to your page.
    <script src="js/fk-shader.js" defer></script>
  3. Give it a size and drop the tag, same as in option A.

Open your page from a server, not by double-clicking the file. Opened as file://, the browser will not let the component read pixels and you get a blank box. Any local server works, for example python3 -m http.server.

Coming from <dv-media> 1.x? See Coming from dv-media 1.x.

Usage

<fk-shader
  src="your-video.webm"
  variant="dots"
  interactive
  alt="The team walking through the office">
</fk-shader>

<style>
  fk-shader { display: block; aspect-ratio: 3 / 4; }
</style>

It needs a size: without a height it collapses to 1 px, shows nothing, and says so in the console. Pass a video or an image file, or #id to draw a video or image already on the page (it reads it, and leaves playing and pausing to you). Videos are recognized by their file extension (mp4, webm, mov, m4v, ogv). For any other URL (blob:, streaming, an API), put a <video id="x"> on the page and pass src="#x". Instances with the same source share one decoder.

Knowing when it is on screen, or why not

// run this after the tag, or from a script with defer
const shader = document.querySelector('fk-shader');
shader.addEventListener('ready', () => { /* first frame drawn */ });
shader.addEventListener('error', (e) => console.log(e.detail.reason)); // 'load' or 'cors'
if (shader.error) console.log(shader.error); // it failed before you were listening

In React, Vue or Svelte

It is a plain custom element, so it works in any framework once the script is loaded. With a bundler, import './fk-shader.js' once in your entry; it does nothing on the server, so SSR is safe. Every attribute is also a property, and booleans treat "false" as off, so interactive={false} does what it says.

In React, listen for ready and error through a ref: an onReady or onError prop does not fire in React 18 or 19. In React 18, use class, not className.

// React 18 and 19
import { useEffect, useRef } from 'react';
import './fk-shader.js';

export function Shader({ clipUrl, isTouch }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    const on = () => { /* first frame drawn */ };
    el.addEventListener('ready', on);
    return () => el.removeEventListener('ready', on);
  }, []);
  return (
    <fk-shader ref={ref} src={clipUrl} variant="lines" interactive={isTouch} alt=""
      style={{ display: 'block', aspectRatio: '3 / 4' }} />
  );
}

In Vue, tell the compiler it is a custom element: set compilerOptions.isCustomElement to (tag) => tag === 'fk-shader'. With TypeScript, fk-shader.d.ts types the element and adds the tag to JSX, for React 18 and 19. Download it into your src/, or add its path to include in tsconfig.json.

Examples

Variants

Seven ways to draw the same picture. Each has its own default pitch, shown under it (default sizes are in the API).

dots · pitch 10
squares · pitch 14
lines · pitch 11
dither · pitch 5
ascii · pitch 12
lcd · pitch 16
glow · pitch 5

Glow paints with light instead of marks: the bright parts glow over the dark, with a halo and a little film grain. Drag across it to part the light.

Theming

Color comes from two CSS custom properties, --fk-shader-bg and --fk-shader-ink, in any CSS color format: hex, rgb, hsl, oklch, a name, even currentColor. Set them on a class, or once at the root so they follow your dark mode. On dark paper the light parts of the picture get the ink. The LCD variant has its own pair, --fk-shader-lcd-bg and --fk-shader-lcd-ink, for the glass and the segments, and so does glow: --fk-shader-glow-bg for the dark behind it and --fk-shader-glow-ink for the color of the light. From that one color it builds the whole ramp, deeper and redder in the shadows, paler and hotter in the highlights.

riso
print
term

Image source

Stills work too. Once the picture settles it stops drawing entirely, until you touch it (lcd digits wake briefly to turn over).

dither · pitch 4
lcd pixels · pitch 12

Interactive

Add interactive and a finger or cursor pushes the cells aside, then a spring brings them home. On an LCD, pressing bruises the glass dark, and on glow the light parts around your finger. On a phone, drag sideways: a vertical drag still scrolls the page.

drag across · dots
drag across · lcd

Proximity

For glow, map="proximity" lights up the nearest person instead of what is bright, like a depth camera: dark hair glows like the face, a bright wall stays dark. It loads a small person-finding model the first time an element asks for it, about 10 MB once per page. People only: with nobody in the picture, glow reads brightness.

map="brightness" · default

Tuning

The same clip, first as it comes, then with one attribute changed each time.

default
pitch="6"
pitch="16"
size="0.6"
invert
fit="contain"

API reference

Attributes

Every attribute is also a property (el.variant = 'lcd'). Booleans are on when present, and "false" counts as off.

AttributeTypeDefaultDescription
srcstringnoneVideo or image URL, or #id of a video or image already on the page. A borrowed video is read, never played or paused. Videos are recognized by their file extension (mp4, webm, mov, m4v, ogv). For any other URL (blob:, streaming, an API), put a <video id="x"> on the page and pass src="#x".
variantdots squares lines dither ascii lcd glowdotsHow each cell is drawn.
pitchnumber, pxdots 10, squares 14, lines 11, dither 5, ascii 12, lcd 16, glow 5Size of the grid cell. For glow, a bigger pitch is a softer picture. Minimum 3. Past a cap the grid opens up by itself, so a big box never freezes the page: 9,000 cells for lines, 12,000 for lcd, 16,000 for ascii, 30,000 for the others.
sizenumber, 0 to 2dots 1, squares 0.95, lines 1.35, glow 1Largest mark relative to the cell, for dots, squares and lines. 1 is as wide as the cell. For glow, how strong the halo around the light is: 0 turns it off.
contrastnumber1Multiplies the variant's tone curve. Above 1 thins the mid-tones, below 1 fills them. Useful range 0.5 to 2.
exposurenumber0Adds or removes ink across the whole picture: positive is darker on light paper and brighter on dark paper. Useful range -0.5 to 0.5.
fitcover containcoverCrop to fill, or show the whole frame.
invertbooleanfalseSwap which end of the tone scale gets the ink.
interactivebooleanfalseCells react to touch and cursor. Off under reduced motion and while paused, and then swipes are not captured either. Only an interactive instance holds sideways drags; the rest leave swipes and pinch-zoom to the page.
lcddigits pixelsdigitsFor the LCD variant: seven-segment numbers or plain pixels.
mapbrightness proximitybrightnessFor glow: what lights up. proximity lights up the nearest person with a person-finding model that loads the first time an element asks for it (about 10 MB, from cdn.jsdelivr.net and storage.googleapis.com). With nobody in the picture, or if it cannot load, glow reads brightness. See Proximity.
pausedbooleanfalseFreezes the picture, and it stops reacting to touch.
altstring""Accessible description. Leave empty when decorative, and assistive tech skips it.

CSS custom properties

Any CSS color format works, including currentColor and light-dark(). A value that is not a color logs a warning and falls back to the default.

PropertyDefaultDescription
--fk-shader-bgtransparentPaper. Its lightness decides which end of the picture gets ink. Left transparent, the ink decides instead: light ink is read as a dark page.
--fk-shader-inkthe element's colorThe marks.
--fk-shader-lcd-bg, --fk-shader-lcd-ink#A6AA80 #1F241BGlass and segments for the LCD variant.
--fk-shader-glow-bg, --fk-shader-glow-ink#0B0908 #F27A1EThe dark behind the glow variant, and the color of its light. Shadows run deeper and highlights paler from that one color. With a light background the dark parts of the picture glow instead.

Methods, events and parts

NameDescription
play(), pause()Same as removing or setting paused.
ready event, el.readyFires when a source draws its first frame, once the element is on screen, and again after each src change. Bubbles. el.ready stays true until the source changes, so a listener added late can check it.
error event, el.errorThe source could not be loaded (detail.reason is load), or it is on another origin and failed or cannot be read (cors). Also logged once as a warning that says what to do. Fires a tick later so a listener added right after the tag hears it, and does not bubble. el.error holds the reason, or null.
::part(canvas)Style the internal canvas from outside.
window.FkShaderThe element class, for instanceof checks.

Accessibility and performance

With an alt the canvas is an image with that label. With an empty alt it is hidden from assistive tech as decoration. With reduced motion on, the picture is drawn once without easing, a video it loaded stays paused, and interactive is off. The page's Pause button above does the same for everyone.

It only draws when something changes: a new video frame, cells settling, a finger on it. Offscreen instances stop, and a still image stops drawing once it settles (lcd digits wake briefly to turn over). Cost grows with the number of cells, and halving the pitch makes four times as many, so a small pitch on a big box is the expensive combination.

Media from another domain needs CORS headers, or the browser will not let the component read its pixels (you get an error event with cors, and a console warning that names the missing header). It works under a strict Content Security Policy: its styles use a constructed stylesheet, not an inline style tag. The one exception is map="proximity", whose model needs cdn.jsdelivr.net and storage.googleapis.com allowed, and WebAssembly ('wasm-unsafe-eval'); without them glow reads brightness.

Coming from dv-media 1.x

This component used to be called <dv-media>. Nothing breaks if you leave it: the old tag and its pinned links keep working as they are. The rolling /dv-media.min.js is frozen at 1.2.0 and gets no more fixes. To move to 2.2.0, do it in this order:

  1. Replace the whole script line with the new one from Installation. Do this first, and not with a find-and-replace of "dv-media": that would also rewrite the old address, and point it at a file that does not exist. If you copied the file, download fk-shader.js in its place.
  2. Rename the tag, <dv-media> to <fk-shader>, in your HTML and in any CSS or script that selects it.
  3. Rename the color variables, --dv-bg to --fk-shader-bg, and the same for --dv-ink, --dv-lcd-bg and --dv-lcd-ink. Attributes, events and variants stay the same.
  4. window.DvMedia is now window.FkShader. With TypeScript, swap dv-media.d.ts for fk-shader.d.ts and rename DvMediaElement, DvMediaVariant, DvMediaErrorReason and DvMediaAttributes to FkShaderElement, FkShaderVariant, FkShaderErrorReason and FkShaderAttributes.

Lab

The playground has 31 styles, layers and brushes. Tune a look there, then copy it as a tag with Copy as <fk-shader>, in More, Component. Looks that use a style outside these seven copy as the closest variant. Layers (Marks, Captions, Grain and the rest) and the Lab's color modes stay in the Lab.

Open the Lab