Regular markup with data-vane-* attributes — the baseline every analytics tool should handle.
content tracking · exposure 2000ms
Events, emitted. Nothing sent.
This hero is marked with data-vane-content="hero-banner" and
data-vane-context-* attributes. Check the event payload for
context. A content_serve event fired the moment it entered the DOM,
and after it has been visible for 2 cumulative seconds, a content_view appears.
emitscontent_servecontent_viewcontent_click
content tracking · default exposure 1000ms
Feature showcase
Each card below has its own data-vane-content-click identifier.
👁️ Viewport detection
IntersectionObserver-based, with large-content handling.
🪪 Cookie-free IDs
localStorage client & session IDs. Zero cookies set.
📊 Web vitals
FCP, LCP, CLS and FID as standalone web_vitals event at page end.
🔌 Zero backend
You decide where events go. GA4, PostHog, anywhere.
emitscontent_servecontent_viewcontent_click
large content · 65% viewport-fill rule · exposure 3000ms
A deliberately tall article
This article is taller than your viewport, so it can never be 100% visible at once.
Weathervane switches to percentage-based detection: the article counts as "visible" while it
fills at least 65% of your viewport. Scroll through it slowly and after 3 cumulative
seconds of qualifying visibility, a content_view fires.
Section one. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec ullamcorper nulla non metus auctor fringilla. Maecenas faucibus mollis interdum.
Section two. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Aenean lacinia bibendum nulla sed consectetur. Nullam id dolor id nibh ultricies vehicula ut id elit.
Section three. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum. Sed posuere consectetur est at lobortis. Etiam porta sem malesuada magna mollis euismod.
Section four. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
Section five. Donec id elit non mi porta gravida at eget metus. Nulla vitae elit libero, a pharetra augue. Maecenas sed diam eget risus varius blandit sit amet non magna. Curabitur blandit tempus porttitor.
Section six. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Sed posuere consectetur est at lobortis. Vestibulum id ligula porta felis euismod semper. Integer posuere erat a ante venenatis dapibus posuere velit aliquet.
emitscontent_servecontent_viewcontent_click
form tracking · engage, submit + abandonment
Sign up form
Focus a field to see form_engage, then submit to see form_submit (with completion_time).
Or focus a field, then click outside the form — after 3 seconds of inactivity, form_abandon fires with the engagement time. Re-focusing resets the timer.
emitsform_engageform_submitform_abandon
automatic link tracking
Links
Every anchor click emits link_click with the URL, text, target, and whether
it's external. No attributes needed. (These are safe to click — they won't leave the page.)
Content added via JavaScript is discovered automatically — no re-init needed.
emitscontent_servecontent_viewcontent_click
Part 2 · Shadow DOM
The same data-vane-* attributes, inside web components — where most analytics
tools go blind. Every condition below is handled automatically.
imperative shadow root · created before init · content + link + form
Component rendered at load
<vane-hero-widget> below calls attachShadow() while the page
is still parsing — before Weathervane initializes. The initial scan finds the root anyway.
Its shadow tree contains tracked content, a CTA, a link, and a form: all four event flows work
through the boundary.
declarative shadow dom · parsed at page load · zero javascript
Declarative shadow DOM (static)
This root was created by the HTML parser from a
<template shadowrootmode="open"> in the page source. No JavaScript created
it — there was never an attachShadow() call to intercept.
📜 Static DSD block — parsed straight from the HTML source.
emitscontent_servecontent_viewcontent_click
nested shadow roots · two levels deep
Component inside a component
<vane-nested-outer> renders <vane-nested-inner>
inside its own shadow root. Both levels carry tracked content — discovery recurses through
every open root it finds.
emitscontent_serve ×2content_view ×2content_click
cross-boundary attribution · composedPath()
Click inside, container outside
The card below is a light-DOM content container
(data-vane-content="cross-boundary-card"), but its button lives inside a web
component's shadow root. The click walks event.composedPath() and attributes to
the outer container — check the content_name on the event.
Light-DOM card
My button is rendered by <vane-cta-button>, a shadow boundary away
from where my container is declared.
<vane-closed-widget> uses attachShadow({ mode: 'closed' }).
Its shadow tree contains the exact same tracked attributes as the components above — but
closed roots are private by design: events are retargeted beyond recovery and the tree is
unreachable. Click its button and watch the console stay silent.
emitsnothing — closed roots are private, by design
post-load injection · both root-creation paths
Components that arrive later
Two different ways a shadow root can appear after load:
attachShadow() (intercepted by instrumentation) and parser-created declarative
roots via setHTMLUnsafe() (discovered on insertion). Both are tracked seamlessly.
emitscontent_servecontent_viewcontent_click
Part 3 · Page-level behaviors
Tracking that isn't tied to any DOM tree.
spa navigation · pageview_dynamic
Single-page app navigation
Weathervane patches the History API and listens for hash changes. Each button below changes
the URL without reloading — watch for pageview_dynamic events with a fresh
page_view_id.
emitspageview_dynamic
custom events & api
Manual API
Anything you can't express with attributes, you can track directly:
vane.track('video_played', {...}).