Skip to main content
Ask your AI

walker.js

Source code Package

Tag your HTML once, and walker.js does the dataLayer.push for you.

walker.js is one script tag for sites that use Google Tag Manager. You describe your elements with data-elb attributes, and every tagged click, view or form submit arrives in the dataLayer as an event with the same structure, ready for GTM triggers and variables. GTM stays as it is: your container, tags and Consent Mode keep working, walker.js only adds pushes. It is also the quickest way to see walkerOS events on a real page.

By the end of this page, a click on an "Add to cart" button arrives in GTM as product add and goes on to GA4 as add_to_cart. You need GTM on your site and access to its HTML templates, or a developer who has.

Why walker.js​

Today an event reaches GTM in one of two ways. A developer writes a dataLayer.push for it from your tracking spec, or you build a click trigger on a CSS selector that breaks with the next redesign. With walker.js the tracking lives on the element itself.

Without walker.js, code a developer writes and maintains for every event:

<button class="add-to-cart">Add to cart</button>
<script>
  document.querySelector('.add-to-cart').addEventListener('click', function () {
    window.dataLayer.push({
      event: 'add_to_cart',
      ecommerce: {
        items: [{ item_id: 'tee', item_name: 'Cotton Tee', price: 25 }],
      },
    });
  });
</script>

With walker.js, the same button described with attributes:

<div data-elb="product" data-elb-product="id:tee;name:Cotton Tee;price:25">
  <button data-elbaction="click:add">Add to cart</button>
</div>

A click pushes product add with data: { id: 'tee', name: 'Cotton Tee', price: 25 }. What you gain:

  • No push code in your site. walker.js listens, reads the values from the attributes and pushes. A restyle does not break it, because the tracking sits on the element, not on its CSS classes.
  • One structure for every event. Every name is an entity and an action (product add, newsletter signup), and every push has the same keys, so data.price means the same in every product event.
  • Events without tagging. A page view on every page load and on every route change in single-page apps, and a session start when a visit arrives.
  • No cookies, no storage. walker.js sends your events nowhere but the dataLayer. Your GTM tags decide what leaves the page.
  • Your tagging stays yours. The names are neutral, product add rather than GA4's add_to_cart: you map them once in GTM, and the same attributes work unchanged when you grow into your own setup.

If you need consent rules before any data leaves the page, or want to send to GA4, Meta and other tools without GTM, build your own bundle instead, see Grow with it. The full comparison: walkerOS tagging vs. dataLayer.push.

Install it​

Add the script tag to every page, below your GTM snippet:

<script async src="https://static.walkeros.io/v4.8/walker.js"></script>

It loads async and starts once the HTML is parsed. If your site sets a Content Security Policy, allow https://static.walkeros.io in script-src.

You should see: type your page's address into a new tab, then type dataLayer in the browser console. walker.js has pushed a session start and a page view with the page's path, title and referrer. Reload, and only a new page view arrives, because a session starts only when a visit arrives from another site, a campaign link or directly (session detection). In GTM Preview both appear by name in the event timeline.

Tag your page​

Tag the element that represents a thing, here a product, and the elements that trigger events. This is the product from above, with a view when the page loads:

<body data-elbglobals="pagetype:product">
  <div
    data-elb="product"
    data-elb-product="id:tee;name:Cotton Tee;price:25"
    data-elbaction="load:view"
  >
    <h1>Cotton Tee</h1>
    <button data-elbaction="click:add">Add to cart</button>
  </div>
</body>
  • data-elb names the entity, data-elb-product holds its data. Numbers and booleans arrive typed: price is the number 25.
  • data-elbaction fires events: load:view sends product view when the page loads, click:add sends product add on a click. An element without data-elbaction sends nothing.
  • data-elbglobals adds page-wide values to every event, here globals.pagetype.

You should see: click the button, and dataLayer gets a push with event: "product add" and your data.

A developer adds the attributes once, in the component or template, and every page that uses it is tracked from then on. Name entities and actions in your own business language, see the entity-action approach. More triggers (visible, impression, submit, hover), context, nested entities and the user: HTML attributes.

Events from your own code​

Prefer data-elb attributes wherever an element exists. For events only your JavaScript knows about, such as a video player's callback, call elb(). Add this stub to your page's <head>, before your own code:

<script>
  function elb() {
    (window.elbLayer = window.elbLayer || []).push(arguments);
  }
</script>
player.on('play', function () {
  elb('video play', { title: 'Product tour' });
});

walker.js loads async, so your code may run first: the stub queues those calls until walker.js is ready. The push has the same structure as a tagged event. Details: elb.

What a push looks like​

Each event is one push: the walkerOS event plus event, its name with the space, for GTM's Custom Event triggers, and _clear: true, which makes GTM replace the previous event's values instead of merging them. The product add from above, with the values that change on every push shown as placeholders:

{
  "event": "product add",
  "_clear": true,
  "name": "product add",
  "data": {
    "id": "tee",
    "name": "Cotton Tee",
    "price": 25
  },
  "context": {},
  "globals": {
    "pagetype": "product"
  },
  "custom": {},
  "nested": [],
  "id": "<id>",
  "trigger": "click",
  "entity": "product",
  "action": "add",
  "timestamp": "<timestamp>",
  "timing": "<timing>",
  "source": "<source>"
}

This is the standard walkerOS event, and its structure is stable. What each key holds: event model.

If nothing shows up:

  • Check in the Network tab that walker.js loaded. Ad blockers can block it; self-hosting avoids that.
  • Check that the element, or one inside it, has data-elbaction.
  • walker.js pushes to window.dataLayer. A container set up with a renamed dataLayer does not see the events.

Use it in GTM​

walker.js has no settings: your tagging decides what it sends, and GTM decides what happens next. This section builds one complete example, product add to GA4's add_to_cart, then covers the other events.

You need a Google tag with your GA4 measurement ID (G-...) as its Tag ID, fired on Initialization - All Pages. Most containers already have one.

Example: product add to GA4​

1. Trigger. Triggers, New, trigger type Custom Event:

  • Event name: product add, with the space
  • This trigger fires on: All Custom Events
  • Name it CE - product add

2. Variables. Variables, User-Defined Variables, New, type Data Layer Variable, Data Layer Version Version 2 (the default). Create one per row:

Variable nameData Layer Variable Name
DLV - data.pricedata.price
DLV - datadata
DLV - entityentity
DLV - nestednested

3. Items. GA4 ecommerce needs an items list, and GTM builds lists only with a Custom JavaScript variable. It is the one piece of JavaScript in this setup, it lives in GTM, not in your site, and it serves every product event and orders. Name it JS - walker items:

The JS - walker items variable
function () {
  // A product event is its own item, an order carries its products in nested.
  var list = {{DLV - entity}} === 'product'
    ? [{ entity: 'product', data: {{DLV - data}} }]
    : {{DLV - nested}} || [];
  var items = [];
  for (var i = 0; i < list.length; i++) {
    if (list[i].entity !== 'product') continue;
    var p = list[i].data || {};
    items.push({
      item_id: p.id,
      item_name: p.name,
      price: p.price,
      quantity: p.quantity || 1
    });
  }
  return items;
}

4. Tag. Tags, New, tag type Google Analytics: GA4 Event:

  • Measurement ID: your G- ID
  • Event Name: add_to_cart
  • Event Parameters:
Parameter nameValue
currencyEUR
value{{DLV - data.price}}
items{{JS - walker items}}
  • Triggering: CE - product add

GA4 requires currency whenever value is set. If you sell in several currencies, tag the currency as a global (data-elbglobals="currency:EUR") and read it with a Data Layer Variable for globals.currency.

5. Check it. Open Preview and click the button. Tag Assistant lists product add in the timeline with your GA4 tag under Tags Fired, and its Variables tab shows JS - walker items as [{ item_id: "tee", item_name: "Cotton Tee", price: 25, quantity: 1 }]. GA4's DebugView shows add_to_cart with its item.

Other events​

Every event works the same way: a Custom Event trigger on its name, Data Layer Variables for its values, a GA4 Event tag.

walkerOS eventGA4 eventParameters
product viewview_itemcurrency, value from data.price, items
order completepurchasetransaction_id from data.id, value from data.total, currency, items
session startsession startoptional: data.source, data.medium, data.campaign under names of your own

An order confirmation is tagged like any other element, with its products inside, and the same items variable reads them:

<div
  data-elb="order"
  data-elb-order="id:0rd3r1d;total:50"
  data-elbaction="load:complete"
>
  <div data-elb="product" data-elb-product="id:tee;name:Cotton Tee;price:25;quantity:2"></div>
</div>

session start counts arrivals, while GA4 keeps counting its own sessions, so the two numbers differ. For its parameters, avoid GA4's campaign_* names, which feed GA4's own traffic attribution.

To cover several events with one tag, tick Use regex matching in the trigger, for example ^product (view|add)$, and set the tag's Event Name with a Lookup Table variable whose Input Variable is {{Event}}.

Page views​

walker.js sends a page view on every page load and on every route change in single-page apps. Sending these to GA4 in place of GA4's own page views gives your page views your tagging, such as a page_type from globals.pagetype. On a page load, the page's load events, such as product view, arrive before its page view. To switch:

  1. In the Google tag, under Configuration settings, add the parameter send_page_view with the value false.
  2. In GA4, go to Admin, Data streams, your web stream, Enhanced measurement, Page views, Show advanced settings, and clear Page changes based on browser history events. Otherwise GA4 still counts route changes itself, and each one counts twice.
  3. Add a Custom Event trigger on page view and a GA4 Event tag with Event Name page_view and the parameters page_location from source.url and page_title from data.title, each read with a Data Layer Variable. Leave out page_referrer, GA4 fills it in.
  4. Pause any tag that sends page views on a History Change trigger.

Or keep GA4's own page views and do not forward walker.js's. Never both.

walker.js pushes every event to the dataLayer. Whether a tag may send it on is decided in GTM, with Consent Mode and each tag's consent settings, as today.

Grow with it​

walker.js is fixed on purpose: it fills your dataLayer and nothing else. Most growth needs no new tool, only more tagging. When you want more than GTM can do with the dataLayer, replace the file with your own bundle. Your data-elb attributes and elb() calls stay exactly as they are.

StepYou gainYou run
walker.jsEvents in the dataLayer, no push codeOne script tag
Your own bundleSend to GA4, Meta and other tools directly; mapping that renames events and builds GA4 items; consent rules per destination; sessions across page loads once storage is allowedThe CLI, to build a file you host
A server flowYour own collection endpoint; server destinations such as Meta Conversions API or your warehouseA container or Node.js process

Your own bundle. Write a flow file and build it with the CLI. This flow sends the same events to GTM as walker.js: the session source, the browser source with history for single-page apps, and the GTM destination, which uses the container already on your page:

{
  "version": 4,
  "flows": {
    "default": {
      "config": { "platform": "web" },
      "sources": {
        "session": { "package": "@walkeros/web-source-session" },
        "browser": {
          "package": "@walkeros/web-source-browser",
          "config": { "require": ["session"], "settings": { "history": true } }
        }
      },
      "destinations": {
        "gtm": {
          "package": "@walkeros/web-destination-gtag",
          "config": { "settings": { "gtm": { "containerId": "GTM-XXXXXXX" } } }
        }
      }
    }
  }
}
walkeros bundle flow.json -o ./dist/bundle.js

Replace the walker.js script tag with your file. Your triggers and variables keep working, with one difference: the GTM destination pushes no _clear, so GTM merges each push with the previous one and a key an event does not carry keeps its last value. From here, add what you need: GA4 with mapping, which builds items for you, consent rules, more destinations, or a server flow (flow configuration, deploy).

warning

Never load walker.js and your own bundle on the same page. Both listen to the same attributes, so every click would count twice.

FAQ​

Does it replace GTM?​

No. walker.js fills the dataLayer; your container, tags, triggers and Consent Mode stay as they are.

I can't edit the HTML. Can I still use it?​

Loaded from GTM, walker.js needs no page change and gives you page view and session start. Clicks, views and form submits need data-elb attributes in the HTML, added once per component by whoever builds the templates.

Will it clash with my existing dataLayer pushes?​

walker.js pushes event, _clear and the walkerOS event keys at the top level: name, entity, action, data, context, globals, custom, nested, trigger, id, timestamp, timing, source, and user and consent once they are set. With _clear, each key it pushes replaces what your site pushed under the same name. Keep your own values under other keys, such as ecommerce or page, and use walker.js variables only in tags that walker.js events trigger.

Does it set cookies or store anything?​

No. walker.js sets no cookies, writes no local or session storage and sends events nowhere but the dataLayer. Consent applies where data leaves the page, in your GTM tags.

Does it work with my single-page app?​

Yes, with routers that change the URL through the History API, which most do: every route change sends a page view. Remove elb('walker run') calls on route changes and History Change page view tags, or each navigation counts twice. Details and limits: single-page apps.

Is the push format stable?​

Yes. A push is the standard walkerOS event plus event and _clear, so your triggers and variables keep working across updates.

I used the configurable walker.js. What changed?​

Since 4.7, walker.js reads no configuration: elbConfig, data-elbconfig and createWalkerjs no longer apply, and tracking starts on load. A script tag that loads @walkeros/walker.js@latest gets the new file. To keep a configured setup, pin https://cdn.jsdelivr.net/npm/@walkeros/walker.js@4.6.1/dist/walker.js, or rebuild it as your own bundle.

💡 Setting this up for production?
If your setup involves consent management, server-side pipelines, or custom destinations, the creators of walkerOS offer hands-on implementation support. Start with a free scoping call.