107

Pixel Sort Drag

£10 or the whole Vault, £99

A WebGL pixel sort you paint with the pointer: drag across a photograph and the pixels in your wake smear into luminance-sorted streaks, fine on a slow move and torn wide open on a fast flick, then relax back into the clean image.

advanced
6 more details
webgl-shaderglitchvelocity-reactivepointer-effectshover-effectkeyboard-navigation

This demo reads better at your own screen size than in the frame below:

Released

About this effect

The glitch-art pixel sort, as something you do with your hand rather than a filter you export. Drag across the photograph and a soft band follows the path; inside it, runs of pixels are re-ordered by brightness, so the image smears into dark-to-bright streaks that trail behind the direction you moved. Drag sideways and the rows sort; drag up or down and the columns do.

Read the full effect overview

Pointer speed decides how much of the image counts as sortable. A slow, careful move sorts only a slice of the tones in short runs, leaving fine controlled streaks; a fast flick opens the threshold to almost everything and the band blows wide. Hold the drag and the stroke stays sorted. Let go and it relaxes: the threshold closes from the shadows up and the runs retract back into the photograph over about a second, with a faint streak lingering on the brightest highlights last.

It works on any photograph you give it, because the threshold adapts to that image's own tonal range, and it falls back to the plain photograph without JavaScript, without WebGL, or under reduced motion.

Pixel Sort Drag - GSAP animation effect preview

What's included

9 items
  • A real luminance sort, not a displacement: every streak is made of the photograph's own pixels, re-ordered darkest to brightest along the run
  • Rows or columns, chosen by the direction you drag, with the bright end of each run leading the stroke
  • Pointer speed opens the sort threshold, widens the band and lengthens the runs, so slow moves stay fine and a flick tears the frame open
  • The band is painted into a trail that follows the actual pointer path, and holds for as long as the drag does
  • An eased release: the threshold closes from the shadows up so the streaks retract into the image rather than fading over it
  • A faint residual streak on the brightest highlights that outlasts the band
  • Threshold measured from each photograph's own tonal range, so a low-key portrait and a bright landscape sort with the same settings
  • Touch, a slow autonomous stroke between touches on phones, and arrow-key sweeps for the keyboard
  • Draws nothing at rest: the loop sleeps once the image is clean, runs only on screen, and releases the WebGL context on teardown

Perfect for

4 use cases
  • Music, fashion and editorial landing pages that want one tactile, unmistakable hero moment
  • Photographer and art-director portfolios where the image itself is the interaction
  • Campaign and launch pages that borrow the glitch-art register without a video file
  • Record sleeves, event posters and drop pages built around a single striking photograph

How it works

3 sections

The photograph is drawn on a single full-bleed plane with three.js. A small, low-resolution trail texture follows the pointer: each frame it paints a soft stroke from the previous pointer position to the current one, recording how hard, how fast and in which direction you moved, and each frame it forgets a little, at a rate measured in real time so it behaves the same at 30, 60 and 120Hz.

Wherever that trail is present, the fragment shader performs a bounded pixel sort. It looks along the row, or the column for a vertical stroke, finds the stretch of pixels whose brightness falls inside the current threshold, and replaces each pixel with the value it would hold if that stretch were sorted by luminance. The threshold, the length of the stretch and the width of the band all come from the recorded speed, and as the trail fades the threshold closes, which is what makes the release a retraction rather than a cross-fade. Outside the trail the plane is a single untouched texture read.

GSAP handles the responsive and reduced-motion branching, the teardown, the fade-in and the played strokes (the intro pass and the keyboard sweeps). The trail, its decay and the sort run on the renderer's own frame loop.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying Pixel Sort Drag opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse free effects

Or the whole library: the Vault, £99, one payment.

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

The inline script is not optional. The photograph in the markup is the fallback for no JavaScript, no WebGL and reduced motion, and without this it paints for the half second it takes three.js to arrive, then gets swapped for the canvas. The probe runs before first paint, stamps html.gl when a canvas is coming, and the stylesheet hides the fallback under that class. No JavaScript, no class, photograph shows; the effect removes the class again if it cannot build a renderer after all. It probes WebGL2 because that is what current three.js requires.

2. Add to your <body>:

Code snippet omitted: it ships with the download.

3. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

The script reads the <img>, drops a canvas in front of it, and hides the figure once the canvas is actually there.

Why the import map rather than a plain <script src> for three.js: three ships as ES modules only, and its old UMD build logs a deprecation warning on every page load. The shim above imports it as a module, puts it on window, and then loads assets/script.js as an ordinary script, so the effect itself stays a plain file you can drop into any build (or none).

Already using three.js as a module? Then skip the shim, and just make sure window.THREE is set before assets/script.js runs:

Code snippet omitted: it ships with the download.


Choosing Your Images

This effect has a real content requirement, and ignoring it is the main way to make it look broken. A pixel sort re-orders the pixels along a row by brightness, so it only shows where brightness changes along that row. A flat sky sorted is the same flat sky. So:

  • Tonal variety across the frame, not just contrast. A subject with dark masses (hair, silhouettes, clothing) against mid-tone gradients (gel light, a sunset, skin, painted walls) gives the sort something to drag through. Large uniform areas stay untouched no matter how fast you flick.
  • Strong structure perpendicular to the stroke. Horizontal drags smear across vertical edges (a face in profile, trees, architecture) most visibly; vertical drags do the same to horizons and shelves.
  • Ungraded, or lightly graded. The shader measures the photograph's own darkest and brightest tones and works inside that range, so a low-key image sorts fully, but a heavily crushed grade leaves little in the middle to re-order.

Test with a fast flick through the subject, not at rest. The fallback and the canvas cover-fit the image, so any aspect ratio fills the frame.

The demo photograph is a derivative of Pexels photo 4985860 (Pexels licence), resized and encoded from assets/img-src/ by assets/img-manifest.json.


Using It With Your Own Design

What the effect needs from your markup: an element with data-pixel-sort, containing an element with data-pixel-sort-source that holds one <img>. The script reads that image as its texture (no second download) and uses its alt text as the canvas's accessible name. Every data-pixel-sort on the page becomes its own instance.

What is only the demo's styling: the floating prompt pill, the dark page ground, the crosshair cursor and the Mona Sans font. Delete them freely; the effect draws nothing outside its own root.

CSS the effect depends on:

  • The container must have a real size. The canvas is sized from the container's clientWidth/clientHeight, so .pixel-sort needs a height from somewhere: it is position: absolute; inset: 0 in the demo. Give it a height of 0 and you get a canvas of 0. It tracks later size changes on its own.
  • The .pixel-sort__source figure must stay in the document. It is the fallback for no JavaScript, no WebGL and reduced motion, and it is hidden only by html.gl (the head probe) and .pixel-sort.is-live, a class the script adds after a canvas exists. Style it as the photograph it is.
  • touch-action: pan-y on .pixel-sort. Sideways drags sort; vertical swipes keep scrolling the page on touch. If the effect is a full-screen hero that should own the whole gesture (so vertical drags sort columns on phones too), change it to none.
  • overflow: hidden and position: relative or absolute on the root, so the canvas (position: absolute; inset: 0) sits inside it.

Options

All options are data attributes on the data-pixel-sort element.

Attribute Values Default Description
data-threshold 0 to 1 0.2 Darkest tone that sorts on a slow move, as a fraction of the photograph's own range. Lower sorts more of the image even on gentle strokes.
data-threshold-fast 0 to 1 0.02 Darkest tone that sorts on a full-speed flick.
data-band 0.02 to 0.3 0.075 Radius of the band on a slow move, as a fraction of the height.
data-spread 1 to 4 2.2 How much a full-speed flick widens the band. 1 keeps it constant.
data-reach 4 to 40 22 Pixels between run samples at full speed. The longest streak is about 21 times this.
data-speed-scale px per second 1400 Pointer speed that counts as a hard flick on a 1200px-wide effect (scaled with the width, so a phone needs a proportionally shorter flick). Raise it to make the band harder to blow open.
data-split 0 to 3 1 Red and blue channel delay along the streak at speed. 0 keeps colours registered.
data-decay 0.9 to 0.995 0.972 How much of the band survives each 60th of a second after release. Higher settles more slowly.
data-hold 0.9 to 1 0.997 The same while the pointer is held down. 1 keeps the whole stroke sorted until release.
data-linger 0.9 to 0.999 0.99 Decay of the residual highlight streak.
data-highlight 0 to 1 0.72 Tone above which the residual streak shows.
data-residual 0 to 1 0.8 Strength of the residual streak. 0 turns it off.
data-intro 1, 0 1 One played stroke across the image when it loads.
data-auto 1, 0 1 On touch devices, a slow autonomous stroke between touches.
data-max-dpr number 2 Pixel ratio cap on fine pointers (touch is always capped at 1.5).

Example: a calmer sort that needs real speed to open

Code snippet omitted: it ships with the download.

Example: long, wild streaks that settle slowly

Code snippet omitted: it ships with the download.


Accessibility

  • Reduced motion: the WebGL scene never starts. The photograph stays exactly as it is, full-bleed and still, with its alt text. Switching the preference while the page is open tears the canvas down and shows it.
  • Without JavaScript, or without WebGL: the same photograph, for the same reason. One fallback, three failure modes, and it is never hidden until a canvas has actually been created.
  • Keyboard: while the canvas is live the root is focusable, with a visible focus ring. The arrow keys play a sweep across the middle of the image in that direction, so the keyboard reaches both the row and the column sort.
  • Touch: dragging a finger drives the same band a mouse does; between touches on phones a slow autonomous stroke keeps the image alive (data-auto="0" turns it off).
  • Screen readers: the canvas carries role="img" and the photograph's alt text.

Dependencies

Dependency Version Required
three.js 0.180.0 Yes, as an ES module (see Quick Start)
GSAP core 3.15.0 (3.12+ works) Yes: matchMedia branching, teardown, the load fade and the played strokes (intro and keyboard sweeps)

No GSAP plugins. The sort itself, the trail and its decay run on the renderer's own frame loop, not on GSAP; GSAP moves a virtual pointer for the played strokes, which the loop reads exactly like a real one.

Everything the effect uses (WebGLRenderer, WebGLRenderTarget, ShaderMaterial, PlaneGeometry, OrthographicCamera, Texture) is long-stable three.js API, so pinning to a different version is a one-line change in the import map.


Browser Support

Anything with WebGL2, which is every current browser. Support is probed before three.js is asked for a renderer, deliberately: three logs its own failure to the console, as errors, several times over, before it throws, so catching the exception would hide nothing and a visitor with WebGL disabled would get a console full of red on a page that had quietly fallen back. Probed first, that visitor simply gets the photograph and a clean console.

Import maps are supported everywhere current. In a browser old enough to lack them the module never runs, window.THREE is never set, and the photograph is again what shows.

Performance

Two draw calls a frame: the trail update at a quarter of the resolution, and the image plane. At rest the plane is a single texture read per pixel, and once the image has settled back to clean the loop stops drawing entirely until the next input. The expensive part is the sort, which only runs inside the band: each sorted pixel reads a fixed window of 21 samples along its row and ranks them. The loop is gated on an IntersectionObserver and does not run at all while the effect is off screen.

To buy back frames on low-end hardware, lower data-max-dpr first (to 1.5, or 1): the sort costs per device pixel, so that is the largest saving. After that, a smaller data-band and data-spread shrink the area being sorted.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.