threadtext
Text,
embroidered.
Give it a word in any loaded font and it renders as raised satin floss on a transparent ground — threads that run across each stroke and fan around the curves, lifted into 3D and sewn in one satin stitch at a time. Drop it over any background. Photorealistic by construction, from the font’s real glyph geometry — not a raster filter, not an AI image.
Live demo — type a word
How it works
Glyphs drive the flow
The word is rasterised, then an exact distance transform gives a flow field — smoothed in double-angle orientation space so opposite edge-normals reinforce. Threads run across each stroke and fan cleanly around the curves, with no moiré.
Thread, lit in 3D
Thousands of pre-shaded thread sprites are laid along the flow and lifted into 3D by a dome-shade normal map, so each strand catches the light along its length. The floss sits on a transparent ground — drop it over any background — and a cursor-following sheen turns the threads over as you move.
Sewn in, one row at a time
A breadth-first pass over the stitch graph reveals the word one satin cross-row at a time — the needle following each stroke, letters filling left to right. Typing re-embroiders and animates only the newly-added letters.
Accessibility & motion
The canvas is decorative; the word is exposed to assistive tech via role="img" and an aria-label. prefers-reduced-motion: reduce is honoured — the sew-in is skipped and the word is drawn instantly.
Usage
Drop-in component
import { ThreadText } from '@liiift-studio/threadtext'
<ThreadText text="Thread" font='"Your Font", serif' weight={680} />Hook
import { useThreadText } from '@liiift-studio/threadtext'
const ref = useThreadText({ text: 'Thread', font: '"Your Font", serif', weight: 680 })
<div ref={ref} />Vanilla JS
import { createThreadText } from '@liiift-studio/threadtext'
// Load the face first (any @font-face / next/font / CSS Font Loading API), then:
const thread = createThreadText(document.getElementById('host'), {
text: 'Thread', font: '"Your Font", serif', weight: 680,
})
thread.setText('Threads') // appended letters sew in; unrelated text re-sews
thread.replay() // re-run the full sew-in
thread.destroy() // cancel rAF, remove listeners, free canvasesOptions
| Option | Default | Description |
|---|---|---|
| text | — | The word (or short phrase) to embroider. Required. |
| font | 'Georgia, serif' | CSS font-family of an already-loaded font. The glyph geometry drives the stitch flow — load it however you like before calling. |
| weight | 680 | Numeric font weight (100–900). |
| threadColor | '#fffbf3' | Floss colour — the lit crest of each thread. Hex or rgb(). Changes live, no re-stitch. |
| fill | 0.9 | Fraction of the container width the word spans — its size. The word re-fits to width (with the remainder as padding) on load and resize. |
| pitch | auto | Thread spacing in px. Smaller = finer, denser stitching (and more work). Derived from the fitted height if unset. |
| sewRate | 110 | Satin cross-rows laid per second during the sew-in. |
| sheen | true | Cursor-following radial sheen on the overlay canvas. |
| animate | true | Play the sew-in on mount and on replay(). When false (or reduced-motion), the word is drawn instantly. |
| editable | false | Make the surface focusable and typeable — click to focus, type to edit, Backspace to delete, Enter to replay. Shows a caret; pair with onTextChange. |
| reducedMotion | auto | Force reduced-motion. Auto-detected from prefers-reduced-motion if omitted. |
no-code
Use it in Webflow & Framer
The same effect, no build step — drop it straight into your design tool.
Webflow
One script tag, then mark any element with data-threadtext. Configure it with data-* attributes.
<!-- Site Settings → Custom Code → Footer, or an Embed element -->
<script src="https://cdn.jsdelivr.net/npm/@liiift-studio/threadtext/dist/threadtext.webflow.min.js"></script>
<!-- Then add data-threadtext to any text element -->
<h1 data-threadtext>Your headline</h1>Framer
Insert → Code → New Component, then paste ThreadText.tsx ↗. It imports the core from esm.sh and exposes every option in the property panel — no build step.
import { /* core */ } from "https://esm.sh/@liiift-studio/threadtext"