Point at anything.
lookhere draws a hand-drawn arrow, a handwritten note and pen marks onto any element of a web page, and draws them on like someone sketching. It is one small file (3.87 KB gzipped) with no dependencies.
Get started
Load it straight from the web. No build step, no install:
<script type="module"> import { point } from 'https://lookhere.toolkin.tech/lookhere.js' point('#signup', 'Start here') </script>
Or install it with npm and import it in your app:
npm i lookhere
import { point } from 'lookhere' point('#signup', 'Start here')
That's it. By default it waits until the element scrolls into view, then draws the arrow, then fades the note in. The first argument is a CSS selector or an element; the second is the note. Leave the note empty to draw only a mark.
No-code sites: Webflow, Framer, plain HTML
In Webflow, plain HTML and most site builders, add a data-lookhere attribute to anything, then add one script to the page. Every option has an attribute version: data-lookhere-mark, data-lookhere-side, data-lookhere-color and so on.
<a href="/signup" data-lookhere="Start here" data-lookhere-mark="circle">Sign up</a> <script type="module" src="https://lookhere.toolkin.tech/auto.js"></script>
Framer
Framer adds behaviour to a layer with a Code Override. You make it once and reuse it.
- In the right panel, open Code Overrides, choose New File, and name it
Lookhere. - Replace everything in the file with the code below. Change the note text if you like.
- Select your button or text layer, and under Code Overrides pick the file
Lookhereand the overridewithLookhere. - Open the preview: the arrow draws itself when the layer scrolls into view.
import { forwardRef, useEffect, useRef } from "react" // One override per note: copy this function, rename it, change the text. export function withLookhere(Component): any { return forwardRef((props, _ref) => { const ref = useRef(null) useEffect(() => { let p import("https://lookhere.toolkin.tech/lookhere.js").then(({ point }) => { p = point(ref.current, "Start here", { mark: "circle" }) }) return () => p?.destroy() }, []) return <Component {...props} ref={ref} /> }) }
Webflow
- Select the element, open Element Settings, and under Custom Attributes add
data-lookherewith your note. - Open Page Settings (or Site Settings for every page), and paste the script tag into "Before </body> tag".
- Publish the site to see it.
Marks
A mark is drawn on the element itself, before the arrow. Use it alone, or with a note.
point('#price', 'Was $12', { mark: 'circle' }) // loop around it, arrow points at the loop point('.new-feature', '', { mark: 'box' }) // just a box, no arrow or note point('h1 em', '', { mark: 'underline' }) point('.key-line', '', { mark: 'highlight', color: '#ffd84d' })
Options
| Option | Default | What it does |
|---|---|---|
mark | '' | 'circle', 'box', 'underline' or 'highlight'. Drawn on the element first. |
side | 'auto' | Where the note sits: 'top', 'right', 'bottom', 'left'. Auto goes beside the element when there's room, else above or below. |
trigger | 'view' | 'view' draws when scrolled into view, 'load' right away, 'manual' waits for show(). |
color | red | Any CSS colour, including currentColor and CSS variables. |
width | 2.5 | Pen size in pixels. The stroke still tapers like a real pen. |
curve | 0.6 | How much the arrow bends. 0 is nearly straight. |
roughness | 1 | How shaky the hand is. 0 is steady, 3 is a hurry. |
distance | 76 | Space between the note and the element, in pixels. |
offset | [0, 0] | Nudge the note by x and y pixels. |
seed | from the text | Same seed, same drawing, every time. Change it for a different hand. |
class | none | Extra class names on this one, for styling it on its own. |
Methods
point() returns a small object you can keep:
const tip = point('#export', 'New!', { trigger: 'manual' }) tip.show() // draw it on tip.hide() // fade it out tip.update({ text: 'Try it', color: '#3e63dd' }) // change anything, live tip.destroy() // remove it and clean up tip.el // the drawing's element, if you need it
For no-code pages, auto() does the attribute scan and returns one of these per element: import { auto } from 'lookhere'.
React, Vue, Svelte
Call point() once the element is on the page, and destroy() when it leaves. In React:
import { useEffect, useRef } from 'react' import { point } from 'lookhere' export function SignUp() { const ref = useRef(null) useEffect(() => { const p = point(ref.current, 'Start here', { mark: 'circle' }) return () => p.destroy() }, []) return <button ref={ref}>Sign up</button> }
Vue: call it in onMounted and destroy in onUnmounted. Svelte: call it in onMount and return the destroy. Next.js and other server-rendered setups: it only runs in the browser, so call it inside the effect, never at the top of a file.
Styling
Everything is plain CSS. The drawing takes the color of .lookhere; the note is .lookhere-note.
.lookhere { color: var(--brand) }
.lookhere-note { font: 600 28px/1.1 'Caveat', cursive }
/* one pointer on its own: point('#x', 'Hi', { class: 'quiet' }) */
.quiet { color: #6b6862 }
/* highlighter on a dark page */
.lookhere [data-hl] { mix-blend-mode: screen; opacity: .45 }
The note uses Caveat when your page loads it (from Google Fonts, for example), and falls back to Kalam, Bradley Hand, Segoe Print and Chalkboard SE, so it looks handwritten on every system even without a web font.
Accessibility and motion
- The drawing is decorative and hidden from screen readers.
- The note is linked to the element with
aria-describedby, so screen readers say it with the element, once, in the right place. - When someone has reduced motion turned on, it appears without the drawing animation.
- Notes never take clicks away from your page: the drawing lets the mouse pass through.
Tips for notes people actually read
- Two to four words. "Start here" beats "Click this button to begin your free trial".
- One or two per screen. Arrows lose their pull when everything has one.
- Point at the action, not the decoration: the button, the price, the toggle.
- Use
trigger: 'view'(the default) so the drawing happens while someone is looking.
Browser support
Current Chrome, Edge, Safari and Firefox, on desktop and mobile. Every release is checked automatically in all three browser engines: the arrow ends at its element, follows it when it moves, and skips the animation with reduced motion.