Glyph reveal
A picture that arrives as type. When it comes into view it is first set in a grid of mono glyphs whose weight follows the brightness of the picture: the dark parts in heavy glyphs, the light parts in light ones or in none.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialNothing hides the picture before the script runs: without JavaScript it is simply there. The glyphs are set in the family named by--glyph-reveal-font, Geist Mono here; give the property another mono family the page loads to change it. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Use it on your own elements
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the element on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-glyph-reveal Marks a picture that arrives as glyphs. It takes no value: leave the value field empty. It goes on the Image itself; on a box instead, the script takes the first image inside it. The script lays a canvas over the picture, and the image stays in the page. data-glyph-reveal-state Written by the engine on the picture: waiting while it is covered and out of view, glyphs while the glyph picture prints or stands, sweeping while the front crosses, done at rest, static under reduced motion. Never written by hand. [ Optional ] data-glyph-reveal-layer Written by the engine on the canvas it lays over the picture, which the stylesheet finds by this attribute. Never written by hand. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-glyph-reveal-color #15100c Colour of the glyphs, and of the inverted band at the front. Options: any plain CSS colour. Lighter than background, the picture is set the other way round, light parts in heavy glyphs, so it never reads as a negative. data-glyph-reveal-background #f4ece0 Ground of the glyph stage, what covers the picture until a cell turns. Options: any plain CSS colour, usually the colour of the page behind the picture, so the glyphs seem printed on the page itself. data-glyph-reveal-glyphs .:-=+*#%@ The characters the picture is set in, in any order: the script draws each one and sorts them by the ink they lay down. The space is always there, as the cell with no ink. Options: any string of characters, for instance 01, /-+ or .oO0. Few glyphs give a hard, posterised picture; many give finer shades. data-glyph-reveal-cell 12 Side of one glyph cell, kept in px so the grain is the same on every screen. data-glyph-reveal-hold 0.9 Time from the first glyph to the moment the front leaves, in seconds; the glyphs take 0.45 s of it to print themselves in. data-glyph-reveal-duration 1.4 Time the front takes to cross the picture, in seconds, eased at both ends. data-glyph-reveal-shuffle 0.5 How restless the standing glyphs are, 0 to 1: how often a glyph steps one notch up or down its ramp and back. data-glyph-reveal-direction right Way the front travels. Options: right, left, down, up, or center, which opens from the middle outwards. data-glyph-reveal-trigger scroll What plays the reveal. Options: scroll plays it once as the picture comes into view. hover leaves the picture as glyphs, crosses it while the pointer is on it and goes back when it leaves; on a touch screen it plays as scroll. data-glyph-reveal-replay 0 Whether the reveal plays again each time the picture comes back into view, 0 or 1. Read on scroll only.
Two files and their pictures (3), no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
GlyphReveal.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-glyph-reveal-color #15100c Colour of the glyphs, and of the inverted band at the front. Options: any plain CSS colour. Lighter than background, the picture is set the other way round, light parts in heavy glyphs, so it never reads as a negative. data-glyph-reveal-background #f4ece0 Ground of the glyph stage, what covers the picture until a cell turns. Options: any plain CSS colour, usually the colour of the page behind the picture, so the glyphs seem printed on the page itself. data-glyph-reveal-glyphs .:-=+*#%@ The characters the picture is set in, in any order: the script draws each one and sorts them by the ink they lay down. The space is always there, as the cell with no ink. Options: any string of characters, for instance 01, /-+ or .oO0. Few glyphs give a hard, posterised picture; many give finer shades. data-glyph-reveal-cell 12 Side of one glyph cell, kept in px so the grain is the same on every screen. A fine picture, easier to read, more cells to draw Chunky characters, a picture that only reads from afar data-glyph-reveal-hold 0.9 Time from the first glyph to the moment the front leaves, in seconds; the glyphs take 0.45 s of it to print themselves in. Under 0.45 the front leaves while the glyphs are still printing The glyph picture stands and stirs longer before it turns data-glyph-reveal-duration 1.4 Time the front takes to cross the picture, in seconds, eased at both ends. A quick wipe A slow crossing, the band of inverted cells easier to follow data-glyph-reveal-shuffle 0.5 How restless the standing glyphs are, 0 to 1: how often a glyph steps one notch up or down its ramp and back. At 0 the glyph picture stands perfectly still At 1 the whole picture shimmers data-glyph-reveal-direction right Way the front travels. Options: right, left, down, up, or center, which opens from the middle outwards. data-glyph-reveal-trigger scroll What plays the reveal. Options: scroll plays it once as the picture comes into view. hover leaves the picture as glyphs, crosses it while the pointer is on it and goes back when it leaves; on a touch screen it plays as scroll. data-glyph-reveal-replay 0 Whether the reveal plays again each time the picture comes back into view, 0 or 1. Read on scroll only. At 0 it plays once At 1 the picture is covered again once it has left the window whole, and plays on its way back -
Drive it from JavaScript
When a picture arrives after the page has loaded, when it has to play again from code, or when the page reacts to it being whole.const element = document.querySelector('[data-glyph-reveal]'); element.addEventListener('glyph-reveal:done', (e) => { console.log(e.detail); }); const reveal = GlyphReveal.mount(element); // the picture auto() mounted, or a new one reveal.replay(); // covered again, then played if it is in view reveal.sampled(); // false when the picture could not be read (another origin, no CORS) element.addEventListener('glyph-reveal:done', (event) => { // the picture is whole }); // Pictures added after the page loaded: mount everything marked inside it. GlyphReveal.auto(container); // When the picture leaves the page: reveal.destroy(); // removes the canvas, the listeners and the observers
-
Fit it to your page
- One attribute, on the picture itself. Put
data-glyph-revealon the<img>; it takes no value. On a box instead, the script takes the first<img>inside it. In Webflow, select the Image, open the Settings panel and add the custom attributedata-glyph-revealwith an empty value. - **The
<img>stays the picture.** The script adds one canvas right after it,data-glyph-reveal-layer, laid over the picture's box and never taking a click. The canvas only draws glyphs; a cell that has turned is a hole through which the real<img>shows.alt,srcset, the link around the picture and the right click all keep working. Onscrollthe canvas is removed once the picture is whole. - The crop is the page's.
object-fitandobject-positionare read, so the glyphs set the picture as it is cropped in its box. The canvas takes the picture'sborder-radius. - The typeface of the glyphs is the custom property
--glyph-reveal-fonton the picture or any parent, Geist Mono in the stylesheet. Any family the page loads will do; the script waits for it up to 1.5 s. - One setting for every picture of the page, without opening the file: declare
window.GlyphRevealSettings = { color: '#ffab1a' }in a script placed beforeglyph-reveal.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-glyph-reveal-*attribute on the picture itself (data-glyph-reveal-direction="down"), so one picture can still differ from the page-wide setting. - States, on the marked element, for a style or a test:
data-glyph-reveal-stateiswaiting(covered, not yet in view),glyphs(the glyph picture printing or standing),sweeping(the front on its way),done, orstaticunder reduced motion. - Events, both bubbling,
detail{ element, sampled }:glyph-reveal:startwhen the glyphs start to print, and onhovereach time the pointer comes;glyph-reveal:doneeach time the picture is whole.sampledis false when the glyphs could not follow the picture (see the first pitfall). - From JavaScript:
GlyphReveal.mount(el, overrides)returns the instance, which carrieselement,settings,state(),sampled(),isRunning(),play(),reset()(covered again, waiting),replay(),show()(the picture at once, no motion),resize()anddestroy().GlyphReveal.auto(root)mounts every marked picture insideroot,GlyphReveal.replay(root)plays them again,GlyphReveal.destroy(root)takes them all back. In React,mountinuseEffectanddestroyin its cleanup. - **
destroy()leaves the picture as it was**: the canvas removed, the listeners and observers gone, the state attribute taken off, and theloadingattribute put back as it was written. - Touch:
scrollplays the same on a phone.hoverneeds a pointer that hovers; on a touch screen it falls back toscroll, so the picture is never left as glyphs with nothing to turn it. - **
prefers-reduced-motion**: no canvas at all. The picture is simply there, the state readsstatic, and no event fires. - At rest nothing runs. The loop only turns while a picture is printing, stirring or sweeping, never in a hidden tab, and a picture that is done costs nothing.
- The words of the markup are the preview's, and they ship as they are. Put the user's own words and pictures in their place whenever they want.
- On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- One attribute, on the picture itself. Put
-
Avoid the pitfalls
- A picture from another origin needs CORS, or the glyphs are random. Reading the brightness of a picture means reading its pixels, and a browser refuses that for a picture served from another origin without
Access-Control-Allow-Origin: the canvas is tainted. The engine never throws on it. It first reads the<img>as it is; refused, it asks for the picture a second time withcrossOrigin="anonymous", which a host that sends CORS headers answers (the Webflow CDN does, checked on 03/10/2026); refused again, it sets a random field of glyphs that follows no brightness, and the front still turns it into the picture. The eventdetail.sampledandsampled()say which one played. The browser itself writes one CORS line in the console for the refused second request; that line is not an error of the script. Serving the picture from the page's own origin, or adding the header on the host, gives the real glyph picture. - Before the script runs, the picture shows. Nothing hides it in the stylesheet, on purpose: without JavaScript the picture is simply there. A picture at the very top of a page can therefore show for a moment before the script covers it. Load the script in the head with
defer, or keep the effect for pictures below the first screen, when that flash matters. - The canvas is placed from the picture's box. It follows a resize of the picture and of the window. A picture that moves without changing size while it is still covered (content inserted above it inside an absolutely positioned parent, for instance) keeps its cover where it was until the next resize: call
resize()on the instance after such a change. - **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a picture that already carries the effect returns the existing one untouched. A page-wide block goes inwindow.GlyphRevealSettings, whichauto()reads itself, or the picture is taken back withdestroy()and mounted again. - **The colours go to a canvas, which cannot parse
color-mix()** nor a custom property. Givecolorandbackgrounda plain colour, a hex or anrgb(). - A transformed parent skews the placing. The canvas is placed by comparing two boxes on screen; under a parent scaled or rotated by a transform, the offset is measured in transformed pixels. Put the attribute on a picture whose parents are not transformed while it is covered.
- A very large picture costs its whole area once. The first print and each resize draw every cell; after that a frame only draws the cells that changed. Under 6px,
cellon a full-screen picture is tens of thousands of cells. - An animated GIF or a video poster is read once, at the frame the browser hands over when the picture is ready.
- A picture from another origin needs CORS, or the glyphs are random. Reading the brightness of a picture means reading its pixels, and a browser refuses that for a picture served from another origin without
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Glyph reveal is a scroll animation for Webflow and vanilla JavaScript, with no library. Also called ASCII image reveal, ASCII image effect or ASCII art image hover effect.
A picture that arrives as type. When it comes into view it is first set in a grid of mono glyphs whose weight follows the brightness of the picture: the dark parts in heavy glyphs, the light parts in light ones or in none. The glyphs print themselves in, stand and stir for a moment, then a front crosses the picture on an eased curve, a ragged band of inverted cells at its edge, and every cell it passes turns into the real picture. The glyph sibling of the pixel reveal: where that one writes text in block by block, this one proofs a picture in characters before showing it. The picture itself is never copied: one canvas is laid over the <img>, and a cell that has turned is a hole in it, so the image stays in the page, for search engines and for assistive technology, the whole time. The set of glyphs can be written in any order and in any typeface, the script weighs each one itself. By a setting, the pointer plays it instead of the scroll: the picture then rests as glyphs, the front crosses it while the pointer is on it and goes back when it leaves. Under reduced motion the picture is simply there. Pattern type ASCII image reveal, character mosaic.
Updated
Questions
Select the Image, open the Settings panel and add the custom attribute data-glyph-reveal with an empty value. Settings are more attributes on that same picture, for instance data-glyph-reveal-direction="down". The glyphs are set in the typeface named by the custom property --glyph-reveal-font, Geist Mono in the stylesheet.
No. It is one 2D canvas laid over the <img>, with no library and no build step. The picture is never copied onto that canvas: a cell that has turned is a hole through which the real image shows, so alt, srcset and the link around the picture keep working.
Yes, write data-glyph-reveal-trigger="hover". The picture then rests as glyphs, the front crosses it while the pointer is on it and goes back when it leaves. On a touch screen it plays as scroll, the default, so a picture is never left as glyphs with nothing to turn it.
data-glyph-reveal-glyphs takes any string of characters in any order, .:-=+*#%@ by default, and the script sorts them by the ink they lay down. data-glyph-reveal-cell is the side of one glyph cell in px, 12 by default: lower gives a finer picture with more cells to draw, higher gives chunky characters that only read from afar.
The picture is served from another origin without CORS headers, so the browser refuses to let its pixels be read. The engine then sets a random field of glyphs, and the front still turns it into the picture. Serve the picture from the page's own origin or add Access-Control-Allow-Origin on the host; the Webflow CDN sends that header.