Elastic line
The rules of a page, pulled like strings. Every separator that carries the attribute is caught by the pointer as it comes close or crosses it, bends towards it like a guitar string, slips free once it is pulled too far, and springs back on a damped spring, wobbling until it rests.
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 tutorialA marked rule takes its height and its background from the settingsthicknessandcolor, so a height or a background set on its class is overridden. Its path spills out of its box while it bends: a parent withoverflow: hiddenclose to the rule cuts the bend off. -
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-elastic-line Marks a rule the pointer pulls like a string. It takes no value: leave the value field empty. It goes on an empty div that draws the rule, between two rows, under a heading or above a footer; the stylesheet makes that div a box as thick and as dark as the settings say, as wide as its container. data-elastic-line-state Written by the engine on the rule: live while the line is bent or moving, idle at rest, still under reduced motion. 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-elastic-line-color #15100c Colour of the line, at rest and in motion. Options: any CSS colour, currentColor included, which takes the text colour of the rule's parent. data-elastic-line-thickness 1 Thickness of the line, in px, the height of the box at rest and the stroke of the path in motion. data-elastic-line-bend clamp(24px, 3vw, 44px) How far the pointer can pull the line before it slips free, any CSS unit; it is also the most a line ever bends. data-elastic-line-reach clamp(8px, 1vw, 16px) Distance from the line, above and below, at which the pointer catches it without crossing it, any CSS unit. data-elastic-line-stiffness 380 Tension of the string, the pull that brings the line back to rest, 20 to 2000. data-elastic-line-damping 7 How fast the wobble dies, 1 to 60. data-elastic-line-touch 1 Whether a finger pulls the line, 0 or 1. Options: 1 a finger put on a rule holds it and a tap plucks it; 0 touch is ignored and the rules stay straight on a phone.
Two files, 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
ElasticLine.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-elastic-line-color #15100c Colour of the line, at rest and in motion. Options: any CSS colour, currentColor included, which takes the text colour of the rule's parent. data-elastic-line-thickness 1 Thickness of the line, in px, the height of the box at rest and the stroke of the path in motion. Under 1 a hairline that a plain screen draws pale A bar rather than a rule, and its wobble reads heavier data-elastic-line-bend clamp(24px, 3vw, 44px) How far the pointer can pull the line before it slips free, any CSS unit; it is also the most a line ever bends. A tight string that lets go almost at once and rings small A slack string that follows the pointer across the next rows before it lets go data-elastic-line-reach clamp(8px, 1vw, 16px) Distance from the line, above and below, at which the pointer catches it without crossing it, any CSS unit. At 0 only a pointer that crosses the line catches it The line jumps to a pointer that only passes near, and clings to it along a row data-elastic-line-stiffness 380 Tension of the string, the pull that brings the line back to rest, 20 to 2000. A slow, loose swing, like a rope A fast, nervous ring, like a wire data-elastic-line-damping 7 How fast the wobble dies, 1 to 60. The line rings for seconds Past 2 times the square root of stiffness it comes back without a single wobble data-elastic-line-touch 1 Whether a finger pulls the line, 0 or 1. Options: 1 a finger put on a rule holds it and a tap plucks it; 0 touch is ignored and the rules stay straight on a phone. -
Drive it from JavaScript
When something other than the pointer should pluck a rule, when rules arrive after the page has loaded, or when the page reacts to a line being let go.const element = document.querySelector('[data-elastic-line]'); element.addEventListener('elastic-line:pluck', (e) => { console.log(e.detail); }); const line = ElasticLine.mount(element); // the rule auto() mounted, or a new one line.pluck(40, 0.5); // 40px below the rule at its middle, then it rings line.offset(); // the pull of its tip in px, 0 at rest // Rules added after the page loaded: mount everything marked inside it. ElasticLine.auto(container); // When the rules leave the page: ElasticLine.destroy(container); // every rule inside it goes back to a plain div
-
Fit it to your page
- Structure: an empty
divmarkeddata-elastic-line, wherever the page needs a rule: between two rows, under a heading, above a footer. The stylesheet makes it a box onethicknesstall in the colour of the settings, as wide as its container. Each marked rule is on its own spring; nothing links two rules together. - It is the rule itself, not a border. A
border-bottomon a row cannot bend. Take the border off and put a markeddivin its place. - One setting for every rule of the page, without opening the file: declare
window.ElasticLineSettings = { color: '#ffab1a' }in a script placed beforeelastic-line.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-elastic-line-*attribute on the rule itself (data-elastic-line-color="#ffab1a"), so one rule can still differ from the page-wide setting. - What the script writes: on the rule, the two custom properties
--elastic-line-colorand--elastic-line-thickness, which the stylesheet reads, anddata-elastic-line-state,livewhile the line is bent or moving,idleat rest,stillunder reduced motion. While a line isliveit also carries an inline transparent background and holds onesvg,elastic-line-string; both are gone or hidden at rest. No width and no height is ever written on the rule, and no position either as long as the stylesheet is loaded. - How a line is caught: the pointer comes within
reachof it, or crosses it between two moves. Held, its tip follows the pointer. It slips free once the pointer is further thanbendfrom where the line rests, or past one of its ends, or out of the window. A pointer that crosses a rule too fast to be caught plucks it: one push, as far as it travelled. - Scroll plucks too: when the page scrolls under a resting pointer, a rule that passes under it is caught or plucked the same way.
- **From JavaScript, beyond
mount,autoanddestroy**: the api of a rule carrieselement,settings,pluck(amount, at)which pushes the lineamountpx (below the rule when positive, above when negative) atat, 0 to 1 along it,offset()the pull of its tip in px,frames()andisRunning()for a test that wants to see the loop sleep, andfreeze()andunfreeze(), which keep a line bent where it was let go, for a thumbnail.ElasticLine.destroy(root),freeze(root)andunfreeze(root)act on every mounted rule that isrootor inside it, the document by default. - One event:
elastic-line:pluckon the rule, bubbling, each time a held line is let go.detailcarrieselement,offset, the pull in px at the moment it was let go, andat, 0 to 1 along the line. A page can play a note from it. - **
destroy()leaves the rule as it was**: the path removed, the two custom properties, the state attribute and any inline style taken off. When the last rule of the page is destroyed the listeners on the window go too. - Touch: a finger put within 24px of a rule, or within
reachif it is larger, holds it, through touch events, so the page still scrolls under the finger. Lifting the finger lets the line go. A short touch that barely moves (under 10px and 350ms) is a tap, and plucks the line by a good half ofbend. Withtouchat 0 nothing answers a finger. - **
prefers-reduced-motion**: the rules stay straight, plain boxes in the colour and thickness of the settings. The state readsstill, and turning the preference off brings the effect back without a reload. - The words of the markup are the preview's, and they ship as they are. Put the user's own words 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.
- Structure: an empty
-
Avoid the pitfalls
- **A
div, never anhr.** The script lays its path inside the rule, and anhris a void element that holds no child. - The settings own the colour and the thickness. The stylesheet sets the height and the background of a marked rule from the two custom properties the script writes, so a height or a background given to the rule by a class of the page is overridden. Change
colorandthickness, by attribute or in the settings block. - A parent that clips cuts the bend. The path spills out of the one pixel of its box, up to
bendabove and below. An ancestor withoverflow: hiddenclose to the rule cuts it off; leave it room, or move the clipping further out. - The bent line passes over the text around it. The rule is positioned, so its path paints over the rows above and below while it moves, and never takes the pointer: links under it keep working. Content that must stay above takes
position: relativeand az-index. - A rule needs a width. In a flex row, a marked
divwith no width of its own is 0px wide and catches nothing. Give itflex: 1, or put it in a block or a column. - **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a rule that already carries the effect returns the existing one. A page-wide block goes inwindow.ElasticLineSettings, whichauto()reads itself. Rules added later are mounted withElasticLine.auto(container). - **Two rules closer than
bendare pulled together.** A pointer that pulls one past its neighbour catches the neighbour too. That is the strum; if it is too much for a dense table, lowerbendunder the height of a row.
- **A
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
Elastic line is a cursor animation for Webflow and vanilla JavaScript, with no library. Also called elastic line hover effect, elastic divider or guitar string line effect.
The rules of a page, pulled like strings. Every separator that carries the attribute is caught by the pointer as it comes close or crosses it, bends towards it like a guitar string, slips free once it is pulled too far, and springs back on a damped spring, wobbling until it rests. A list, a table, a footer: each rule is on its own spring, so a pointer run down a page strums them one after the other. At rest a rule is a plain box one pixel thick, which the browser snaps to whole device pixels, so it is crisp on every screen density and takes the width of its container like any other box; an SVG path of the same colour takes its place only while it moves. No library: a small spring integrator, one set of listeners for the whole page and one frame loop that runs only while a line moves. On touch, a finger put on a rule holds it and lifting lets it go, and a tap plucks it. Under reduced motion the rules stay straight.
Updated
Questions
Put an empty div wherever the page needs a rule and give it the custom attribute data-elastic-line. It is the rule itself, not a border and not an hr: a border-bottom cannot bend, and an hr holds no child for the script to lay its path in. Settings go on the rule, for instance data-elastic-line-color="#ffab1a".
No. The engine is a small spring integrator with no library, one set of listeners for the whole page and one frame loop that runs only while a line moves. At rest a rule is a plain box one pixel thick, and an SVG path of the same colour takes its place only while it bends.
data-elastic-line-bend is how far the pointer can pull the line before it slips free, clamp(24px, 3vw, 44px) by default: lower is a tight string that lets go almost at once, higher a slack one. data-elastic-line-damping, 7 by default, is how fast the wobble dies, and a lower value lets the line ring for seconds. data-elastic-line-stiffness, 380 by default, goes from a loose rope to a nervous wire.
Yes. A finger put within 24px of a rule holds it, lifting lets it go, and a short tap plucks it, all through touch events so the page still scrolls under the finger. Write data-elastic-line-touch="0" and the rules stay straight on a phone.
A line that is cut has an ancestor with overflow: hidden close to it: the path spills out of its box, up to bend above and below. A line that does not react is often 0px wide, which happens to a div with no width in a flex row, so give it flex: 1. Under prefers-reduced-motion the rules stay straight on purpose.