the guide

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.thepurvangmehta.com/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.thepurvangmehta.com/auto.js"></script>

Framer

Framer adds behaviour to a layer with a Code Override. You make it once and reuse it.

  1. In the right panel, open Code Overrides, choose New File, and name it Lookhere.
  2. Replace everything in the file with the code below. Change the note text if you like.
  3. Select your button or text layer, and under Code Overrides pick the file Lookhere and the override withLookhere.
  4. 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.thepurvangmehta.com/lookhere.js").then(({ point }) => {
        p = point(ref.current, "Start here", { mark: "circle" })
      })
      return () => p?.destroy()
    }, [])
    return <Component {...props} ref={ref} />
  })
}

Webflow

  1. Select the element, open Element Settings, and under Custom Attributes add data-lookhere with your note.
  2. Open Page Settings (or Site Settings for every page), and paste the script tag into "Before </body> tag".
  3. 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.

circleboxunderlinehighlight
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

OptionDefaultWhat 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().
colorredAny CSS colour, including currentColor and CSS variables.
width2.5Pen size in pixels. The stroke still tapers like a real pen.
curve0.6How much the arrow bends. 0 is nearly straight.
roughness1How shaky the hand is. 0 is steady, 3 is a hurry.
distance76Space between the note and the element, in pixels.
offset[0, 0]Nudge the note by x and y pixels.
seedfrom the textSame seed, same drawing, every time. Change it for a different hand.
classnoneExtra 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

Tips for notes people actually read

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.

Marking up screenshots instead of a live page? Studio does it with no code: open Studio.