Cursor label
A word for the cursor, set on the element itself.
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 tutorialThe stylesheet hides the system cursor over a labelled element while its pill shows, with!important, since a page gives its links a cursor of their own. Fields and frames inside a labelled element keep theirs. -
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-cursor-label="View" The pill says View while the pointer is over this element or anything inside it, and the system cursor is hidden there. On a project in the preview. data-cursor-label="Play" The pill says Play, the same way. On the film in the preview, where data-cursor-label-color and data-cursor-label-word-color on the same element make it cream with an ink word. Any other short word works the same way. data-cursor-label="" Empty, or the attribute with no value: the pill says the default word, the word setting, View unless it was changed. data-cursor-label-state Written by the engine on the html element: on while the pill shows, rest when it does not, off on a touch screen. Never written by hand; the stylesheet reads it to hide the system cursor over the labelled elements. [ Optional ] Any word is a value, and the pill takes its length. The stylesheet draws it in capitals.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-cursor-label-color #ffab1a Colour of the pill. Options: any CSS colour; taken out of this block, --cursor-label-color takes over, then the amber #ffab1a. On one element, data-cursor-label-color. data-cursor-label-word-color #15100c Colour of the word on the pill. Options: any CSS colour; taken out of this block, --cursor-label-word-color takes over, then the ink #15100c. On one element, data-cursor-label-word-color. data-cursor-label-word View The word of an element whose data-cursor-label is left empty. Options: any short word; the stylesheet sets it in capitals. data-cursor-label-font-size 10.5px Size of the word, any CSS unit; the pill grows with it. data-cursor-label-height 2.7 Height of the pill, in times fontSize. data-cursor-label-padding 1.3 Space on either side of the word, in times fontSize. data-cursor-label-lag 0.1 How long the pill takes to catch up with the pointer, in seconds. data-cursor-label-morph 0.3 How long the pill takes to grow, shrink, or ease to the width of a new word, in seconds.
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
CursorLabel.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-cursor-label-color #ffab1a Colour of the pill. Options: any CSS colour; taken out of this block, --cursor-label-color takes over, then the amber #ffab1a. On one element, data-cursor-label-color. data-cursor-label-word-color #15100c Colour of the word on the pill. Options: any CSS colour; taken out of this block, --cursor-label-word-color takes over, then the ink #15100c. On one element, data-cursor-label-word-color. data-cursor-label-word View The word of an element whose data-cursor-label is left empty. Options: any short word; the stylesheet sets it in capitals. data-cursor-label-font-size 10.5px Size of the word, any CSS unit; the pill grows with it. A small tag that barely covers what it points at A large word, a pill that reads as a button data-cursor-label-height 2.7 Height of the pill, in times fontSize. A flat band hugging the word A tall, round pill with air above and below data-cursor-label-padding 1.3 Space on either side of the word, in times fontSize. The ends of the pill close in on the word A long pill with the word small in its middle data-cursor-label-lag 0.1 How long the pill takes to catch up with the pointer, in seconds. It sits on the pointer, the lag hardly shows It trails far behind a quick gesture and drifts into place data-cursor-label-morph 0.3 How long the pill takes to grow, shrink, or ease to the width of a new word, in seconds. It snaps open and shut It opens slowly, with a softer bounce at the end -
Drive it from JavaScript
When a word changes while the pointer is on its element, or when the pill has to come and go with a part of the app.const element = document.querySelector('[data-cursor-label]'); const cursor = CursorLabel.mount(); // the page's engine, running since DOM ready element.setAttribute('data-cursor-label', 'Open'); cursor.refresh(); // the new word shows under a still pointer // When the pill has to go for good: CursorLabel.destroy(); // removes the canvas and the listeners
-
Fit it to your page
- One attribute per element, its value the word. Put
data-cursor-label="View"on any element: a project card, a link, an image, a video block. While the pointer is over it, or over anything inside it, the system cursor is hidden and the pill says the word. Each element says its own:"View"on a project,"Play"on a film, any other short word elsewhere. An empty value, or the attribute alone, says the default word,wordin the settings. - Nested elements: the closest labelled ancestor of what is under the pointer wins, so a card labelled
"Open"can hold a picture labelled"View". - Everywhere else the system cursor stays. Fields, frames and editable text keep it even inside a labelled element: the pill steps aside over them.
- One engine for the whole page. The script mounts itself once on DOM ready and listens on the document, with
pointeroverandclosest(), so an element that gets the attribute later, a card loaded by a CMS or rendered by a framework, works without a call. Loading the script twice starts nothing more. - Settings on one element, or for the page. Every key of the table is also an attribute on the labelled element,
data-cursor-label-<key>in kebab case (data-cursor-label-color="#15100c",data-cursor-label-word-color="#f4ece0"for a dark pill over a pale tile, as on the preview's film). For the whole page, declarewindow.CursorLabelSettings = { word: 'Open' }in a script placed beforecursor-label.js. The three levels, least specific first, areDEFAULTS, then that object, then the element's attributes. The two colours have one more level: without them anywhere, the element's--cursor-label-colorand--cursor-label-word-colorare read, and they are inherited, so a dark section can set them once for every element in it. - The word's face is the stylesheet's: the layer
.cursor-label-layercarries the family, the weight, the spacing and the capitals, Geist Mono 500 in capitals in the file. Its size isfontSize. - Touch and coarse pointers get no pill and no layer at all, and the page behaves as if the script were not there. A device that gains or loses a mouse switches on the spot.
- **
prefers-reduced-motion**: no lag, the pill sits on the pointer, and it only fades in and out; the word changes at once and a press does not tighten it. The engine listens to the setting and switches with it. - Cost at rest is nothing: the loop only runs while the pill moves, grows, shrinks or changes its word, sleeps as soon as it has settled, and stops in a hidden tab.
<html>carriesdata-cursor-label-state:onwhile the pill shows,restwhen it does not,offon a touch screen. - Inside a frame: the engine runs in the page it is loaded in. A page shown in an iframe gets its pill inside the frame, and it goes as the pointer leaves the frame; a frame inside the page keeps the system cursor.
- From JavaScript:
CursorLabel.mount(root, overrides)starts the page's engine, or returns the one already running untouched;rootlimits the labelled elements that count to one part of the page, the whole document by default. The instance carriessettings(the page-wide values, the lengths in px),word()(the word on the pill, empty when none shows),target()(the labelled element under the pointer, or null),refresh()(reads the element under the pointer again, for a word changed while the pointer is on it),frames()andisRunning()(rendered frames since mount, and whether the loop runs, for a test that the loop sleeps), anddestroy().destroy()takes away the canvas, every listener, the loop and the state on<html>, so the markup is as it was and mounting again starts clean.CursorLabel.destroy()does the same. In React, the script loaded once is enough;CursorLabel.mount()in a top-level effect anddestroy()in its cleanup if the pill has to come and go with a part of the app. - The words of the markup are the preview's, and they ship as they are: pasted, the page reads the way the preview did. Put the user's own content in their place whenever they want, in the page's language.
- 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 per element, its value the word. Put
-
Avoid the pitfalls
- **Settings handed to
mount()after the page loaded are dropped.** The script mounts itself on DOM ready, andmount()while the engine runs returns it untouched. Page-wide settings therefore go inwindow.CursorLabelSettings, whichauto()reads itself, or on the elements as attributes, which are read every time the pointer reaches one. - A word changed while the pointer is on its element shows at the next element the pointer reaches; call
refresh()on the instance to show it at once. - Mouse and pen only. A touch pointer never shows the pill, on purpose; a page that fakes pointer events for a test has to give them
pointerType: 'mouse'. - **The layer sits at
z-index: 2147483647** on the body, fixed. A page that puts something above it, or atransformon<body>itself, moves the pill with it. - **The system cursor is hidden with
!important**, on the labelled elements only and only while the pill shows. A labelled element that needs a cursor of its own over one part keeps it by making that part a field or by taking the attribute off it.
- **Settings handed to
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
Cursor label is a cursor animation for Webflow and vanilla JavaScript, with no library. Also called custom cursor with text on hover, view cursor on hover or cursor follower with label.
A word for the cursor, set on the element itself. Any element that carries data-cursor-label="View" (or Play, or any word) turns the cursor into a pill that says it while the pointer is over it, and follows the pointer with a little lag. The pill grows out of the pointer as it arrives and shrinks back into it as it leaves; from one labelled element straight to another it stays, its old word rolls up and out, the new one rolls in from below, and its width eases to the new word. It tightens a little under a press. Everywhere else the system cursor stays as it is. One script for the whole page, watching the document, so elements added later get their word too; one canvas over the window, clicks pass through, and the loop only runs while the pill moves. No pill at all on touch screens, and under reduced motion it follows at once and only fades.
Updated
Questions
Paste the component once, then give any element a custom attribute whose value is the word, for instance data-cursor-label="View" on a project card or data-cursor-label="Play" on a video. An empty value shows the default word, View. Everywhere else the system cursor stays as it is.
No. It is one script for the whole page with no library, drawing the pill on one canvas fixed over the window. Clicks pass through it, and the loop only runs while the pill moves.
Add data-cursor-label-color for the pill and data-cursor-label-word-color for the word on that element. The defaults are #ffab1a and #15100c. For a whole dark section, set the inherited custom properties --cursor-label-color and --cursor-label-word-color on it once.
No. Touch screens and coarse pointers get no pill and no layer at all, and the page behaves as if the script were not there. A device that gains or loses a mouse switches on the spot.
Yes. The script listens on the document with pointerover and closest(), so an element that gets the attribute later, a card loaded by a CMS or rendered by a framework, works without a call.