Menu Search
Jump to the content X X
SmashingConf London Avatar

We use ad-blockers as well, you know. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. our upcoming SmashingConf London, dedicated to all things web performance.

CSS-Only Solution For UI Tracking

The web is growing up. We are building applications that work entirely in the browser. They are responsive; they have tons of features and work under many devices. We enjoy providing high-quality code that is well structured and tested.

But what matters in the end is the impact for clients. Are they getting more products sold or are there more visitors for their campaign sites? The final results usually show if our project is successful. And we rely on statistics as a measuring tool. We all use instruments like Google Analytics1. It is a powerful way to collect data. In this article, we will see a CSS-only approach for tracking UI interactions using Google Analytics.

Further Reading on SmashingMag:

7The Problem Link

We developed an application that had to work on various devices. We were not able to test on most of them and decided that we had to make everything simple. So simple that there wasn’t a chance to produce buggy code. The design was clean, minimalistic, and there wasn’t any complex business logic.

It was a website displaying information about one of the client’s products. One of our tasks was to track user visits and interactions. For most cases, we used Google Analytics. All we had to do was to place code like the example below at the bottom of the pages:

(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),

ga('create', '......', '......');
ga('send', 'pageview');

This snippet was enough for tracking page views, traffic, sessions, etc. Moreover, we placed JavaScript where the user interacts with the page. For example, clicking on a link, filling an input field, or checking option boxes.

ga('send', 'event', 'ui-interaction', 'click', 'link clicked', 1);

The guys from Google handled these events nicely, and we were able to see them in our account. However, at some point the client reported that there were devices that have bad or no JavaScript support. They represented roughly 2% of all the devices that visited the site. We started searching for a solution that did not involve JavaScript. We were ready to admit that we could not collect statistics under these circumstances.

It was not that bad, but the client shared another issue. Our little application was going to be part of a private network. The computers there had JavaScript turned off for security reasons. Also, this private network was important for the client. So, he insisted that we still get stats in those cases. We had to provide a proper solution, but the problem was that we had only CSS and HTML available as tools.

8The Solution Link

While searching for a solution, I was monitoring the Network tab in Chrome’s developer tools when I noticed the following:

Tracking UI with CSS9

(View large version10)

In the beginning, I thought that there was nothing unusual. Google Analytics’s code made few HTTP requests for its tracking processes. However, the fourth column shows the Content-type header of the response. It is an image. Not JSON or HTML, but an image. Then I started reading the documentation and landed on this Tracking Code Overview11. The most interesting part was:

When all this information is collected, it is sent to the Analytics servers in the form of a long list of parameters attached to a single-pixel GIF image request.

So, Google indeed made the HTTP request but not the trivial Ajax call. It simply appends all the parameters to an image’s URL. After that it performs a request for a GIF file. There is even a name for such requests: beacon12. I wondered why GA uses this approach. Then I realized that there are some benefits:

  • It is simple. We initialize a new Image object and apply a value to its src attribute:
    new Image().src = '/stats.gif?' + parameters
  • It works everywhere. There is no need to add workarounds for different browsers as we do for Ajax requests.
  • Tiny response. As Stoyan Stefanov said13, the 1×1px GIF image could be only 42 bytes.

I made few clicks and sent events to Google Analytics. Knowing the request parameters, I was able to construct my own image URLs. The only thing to do in the end was to load an image on the page. And yes, this was possible with pure CSS.

background-image: url('');

Setting the background-image CSS property forces the browser to load an image. Finally, we successfully used this technique to track user actions.

14Tracking User Actions Link

There are several ways to change styles based on user input. The first thing we thought about was the :active pseudo class. This class matches when an element is activated by the user. It is the time between the moment the user presses the mouse button and releases it. In our case, this was perfect for tracking clicks:

input[type="button"]:active {
    background-image: url('');

Another useful pseudo class is :focus. We recorded how many times users started typing in the contact form. It was interesting to find out that in about 10% of cases users did not actually submit the form.

input[name="message"]:focus {
    background-image: url('');

On one page, we had a step-by-step questionnaire. At the end, the user was asked to agree with some terms and conditions. Some of the visitors did not complete that last step. In the first version of the site, we were not able to determine what these users had selected in the questionnaire because the results would have been sent after completion. However, because all the steps were just radio buttons, we used the :checked pseudo class and successfully tracked the selections:

input[value="female"]:checked {
    background-image: url('');

One of the most important statistics we had to deliver was about the diversity of screen resolutions. Thanks to media queries this was possible:

@media all and (max-width: 640px) {
    body {
        background-image: url('');

In fact, there are quite a few logical operators15 that we can use. We can track screens with a specific aspect ratio; devices in landscape orientation; or those with a resolution of 300dpi.

16Drawbacks Link

The problem with this kind of CSS UI tracking is that we get only the first occurrence of the event. For example, take the :active pseudo class example. The request for the background image is fired only once. If we need to capture every click then, we have to change the URL, which is not possible without JavaScript.

We used the background-image property to make the HTTP requests. However, sometimes we might need to set a real image as a background because of the application’s design. In such cases we could use the content property. It is usually used for adding text or icons but the property also accepts an image. For example:

input[value="female"]:checked {
    content: url('');

Because we are requesting an image, we should make sure that the browser is not caching the file. The statistics server should process the request each time. We could achieve this by providing the correct headers. Check out the image below. It shows the response headers sent by Google:

Tracking UI with CSS17

(View large version18)

Sending the following headers guarantees that the browser will not cache the image:

Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

In some cases, we may decide to write our own statistics server. This is an important note that we must consider during development. Here is a simple Node.js-based implementation. We used that for testing purposes:

var fs = require('fs'),
    http = require('http'),
    url = require('url'),
    img = fs.readFileSync(__dirname + '/stat.png'),
    stats = {};

var collectStats = function(type) {
    console.log('collectStats type=' + type);
    if(!stats[type]) stats[type] = 0;

http.createServer(function(req, res){
    var request = url.parse(req.url, true);
    var action = request.pathname;
    if (action == '/stat.png') {
        res.writeHead(200, {'Content-Type': 'image/gif', 'Cache-Control': 'no-cache' });
        res.end(img, 'binary');
    } else { 
        res.writeHead(200, {'Content-Type': 'text/html' });
        res.end('Stats server:<pre>' + JSON.stringify(stats) + '</pre>\n');
}).listen(8000, '');
console.log('Server is running at');

If we save the code to a file called server.js and execute node server.js we will get a server listening on port 8000. There are two possible URLs for querying:

* - shows the collected statistics
* - collecting statistics. 

By requesting the PNG in the second URL, we are incrementing values. The following piece of code shows the HTML and CSS that we have to place in the browser:

<input type="button" value="click me"/>

input[type="button"]:active {
    background-image: url('');

Finally, as a last drawback we have to mention that some antivirus software or browser settings may remove 1×1px beacons. So we have to be careful when choosing this technique and make sure that we provide workarounds.

19Summary Link

CSS is usually considered a language for applying styles to webpages. However, in this article we saw that it is more than that. It is also a handy tool for collecting statistics.

(ds, il, og)

Footnotes Link

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7 #the-problem
  8. 8 #the-solution
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14 #the-tracking
  15. 15
  16. 16 #drawbacks
  17. 17
  18. 18
  19. 19 #summary

↑ Back to top Tweet itShare on Facebook

Krasimir Tsonev is a coder with over ten years of experience in web development. Author of two books about Node.js. He works as a senior front-end developer for a startup that helps people reaching clinical trials. Krasimir is interested in delivering cutting edge applications. He enjoys working in the industry and has a passion for creating and discovering new and effective digital experiences.

  1. 1

    haha wow this is awesome! Especially for click tracking on buttons that don’t have a dedicated JS event where you could put your custom events into.

  2. 2

    Dmitri Tcherbadji

    October 16, 2014 1:42 pm

    I havent even read this yet bit I am super excited already. This is gonna be good!!

  3. 3

    Really great article, Krasimir! Clever in the way you and your team were able to implement this.

  4. 4

    Krasimir Tsonev

    October 16, 2014 1:58 pm

    Thank you guys. I’m glad that you liked the solution.

  5. 5

    Pretty cool!

  6. 6

    Rizky Syazuli

    October 16, 2014 2:14 pm

    wow.. this is brilliant. the situation you presented here is an edge case. but a very clever alternative indeed.

  7. 7

    Nice way to screw people over who are under the impression they won’t get their events tracked by Google Analytics because they use the Analytics Opt-out browser plugin. Not that that ever really opted anyone out of being tracked in the first place, but this way it’s completely obsolete :v

    • 8

      Patrick Denny

      October 16, 2014 3:42 pm

      As a person who builds similar plugins (mine help debug analytics implementations), I can assure you they will still work if you have JS running. These plugins work by listening to the network requests and blocking them is they match specific patterns. where those network requests come from (html, JS, css) is immaterial. This is no different than adding a n<img> to your page to manually send a request to GA.

      as an aside, if you’re blocking JS from running on sites, or blocking page access to localStorage (at least in Chrome) you may also be killing those extensions from working properly. There is a workaround though:

  8. 10

    Patrick Denny

    October 16, 2014 2:26 pm

    Two things I’d like to add
    1) you might want to add “.nojs [rest-of-rule]” to use this technique as last resort only, so that the normal methodology applies when js is available (and remember to remove the .nojs class from body/html via javascript).
    1.a) further to this, adding ” to the page and using it to report back that CSS tracking is being used would give you a metric about how many people aren’t using JS.
    2) in the case of not wanting to overwrite existing backgrounds, you can always use adjacent elements instead with input[name=foo]:checked +/~ .tracker { …. }

    • 11

      Patrick Denny

      October 16, 2014 2:30 pm

      oops. tags got stripped
      1.a) … adding <noscript><span class="css-tracking"></span></noscript> …

      • 12

        Patrick Denny

        October 16, 2014 3:45 pm

        Ooo. better solution, add an image tag inside no script :). <noscript>&ltimg src=”//…”></noscript>

    • 13

      Krasimir Tsonev

      October 16, 2014 2:57 pm

      Great suggestions Patrick. We actually ended up by serving a different version of the site to the people in the private network. So, only they received this hacky version. Anyways, thanks again. I’ll pass this to the team so they know these tips.

    • 14

      Another idea – since noscript is now valid in head for HTML5 I would separate tracking rules to extra css file and incude it in noscript tag – so you don’t need to do the .nojs cleanup …

  9. 15

    Michael Robinson

    October 16, 2014 2:58 pm

    I think the article leaves out the most important reason for using an image: it’s cross-domain. AJAX runs into all kinds of nasty CORS problems, whereas images do not.

  10. 16

    Adrian Roselli

    October 16, 2014 3:11 pm


    Your technique is similar to one I developed to track when users print pages. Since there is no easy way to track printing with JavaScript (and users often don’t click any custom “print” buttons you might add to a page), I call the GA tracking GIF within the print media query. It has the same limitations you outline above, but it provides far better data than any prior methods for tracking print.

    The article at Web Standards Sherpa:
    More technical one at my site:

    One thing to note about background images, if you want to get into an edge of an edge case here, is that Windows High Contrast Mode disable background images by default. It may not be affected by users in the closed environment you identify, but across the general public you might miss a handful of users.

  11. 18

    That’s an interesting technique to get metrics for sort of the “long tail” of users. While 1% or 2% may seem like an edge case, when you have a client that drives that small percentage, you want to provide solutions before someone else does and retain that client.

    With this solution being cross-domain capable and bypassing some other plugins that people might use to maintain their privacy, I wonder how that can be explained to that group of users, or how we can assure people that the technique won’t be misused (or can’t be misused). For instance if one uses Google’s Analytics Opt-Out plugin, that’s really targeting a specific tracking product – does that infer the user doesn’t want any tracking and if so then what about internal or homegrown analytics packages and/or logging mechanisms.

    Personally I think this technique is fine as long as there’s a privacy policy documented somewhere stating what’s tracked, how it’s used, and how it is shared. Then users get to choose whether to use the site or not. I do think it’s within the rights of a company to track users and user behavior as they interact with the product. The gray area is really what they share with others and how they share it. It would be nice to see some sort of consumer privacy act similar to HIPAA, where they are disallowed from sharing data that would be personally identifiable back to you. But that’s probably getting off topic.

    • 19

      Krasimir Tsonev

      October 16, 2014 3:27 pm

      Interesting point of view. From what I know the private network that we (mostly) targeted the site for was informed that we are going to track every user interaction. That was actually one of the main requirements in the beginning of the project.

      However, your comment has a value and I guess that this technique may hit some privacy problems.

  12. 20

    Well, can’t say it isn’t clever. BUT what about the separation from different aspects of a web page? A lifelong battle to avoid mix up css in both html or javascript, and now we have some business logic in a style sheet….

    • 21

      Simone Rescio

      October 16, 2014 5:25 pm

      Agree, two decades of battles telling people that using HTML tables for layout when they are meant to represent tabular data is the worst, and now we advise to use Cascading *Style* Sheets for tracking and business analysis?

      The ones that scary me the most are the ones excited without reading. Of course it can be done, 1px transparent gif tracking has been done before serious JS tracking frameworks came around, of course CSS can be used for this on edge cases, but guys, CSS for tracking *is wrong*.

      Do not propose this at a sprint planning, I would seriously request a new dev to HR the next minute.

      PS: I wouldn’t even accept a gig that requires developing tracking of a no-js intranet html page for 2% traffic, with such requitements we would be still supporting IE5 by now

      • 22

        Well, if it’s used for a special case, I don’t see where the problem is.
        They’re not supporting old browsers; they’re supporting browsers with no JS. And JS is disabled intentionally (security reasons).

        I agree that it’s not something to use over the internet, or to serve a minority (2%). But it’s still a nice hack.
        Good job Krasimir

        • 23

          This hack is old as the internet itself, it’s called pixel tracking, as usually the image is a 1x1px transparent gif, and has been used in email campaigns for tracking views since forever as email clients, desktop and web alike, do not execute javascript and the download of an image is the only way to know how many people actually saw the email as it serves to measure business impact.

          I find just incredible that anyone would rant for 2% of probably non-converting(?) users, no modern browser right now allows to disable javascript as a common user option, firefox removed the option for users in version 23, in chrome you can only disable it from developer tools.

          Edge cases management comes at cost of more development and it makes no sense to invest the money if the ROI is even lover than the money invested, I’m reasoning with an e-Commerce mindset here where such case would be gladly ignored by the business analysts I’ve known so far, the beginning of the article talks about selling products so I thought it was about conversion, was I wrong? Is it really possible that the amount of the other 98% of users collected data wouldn’t be enough of a meaningful sample to take UI and product decisions?

          By the way, CSS media orientation doesn’t indicate the actual orientation of a device calculated by a gyroscope, but simply the ratio between height/width of the browser’s window, which applies to normal pc as well.

          Similarly, the max-width device is applied to the browser window as well, to know about the device the correct property is max-device-width, but relying on that alone, without also the aspect ratio, a landscape smartphone could be mistook for a portrait tablet (, but again would javascript be disabled on mobile devices as well?

          • 24

            I’m absolutely with you on being a purist about segregation of responsibilities, however, we all know, as developers, there are compromises we have to make when the client comes a calling and not all clients are teachable or fireable.

            While this might be an edge case, there are clients who are willing to foot the bill due to their requirements. And yes there are still some companies out their insisting on backwards compatibility to IE5/6. This is just a tool in the toolbelt. Nothing to get riled up over and defame people as bad developers or suggest that they should be fired for wanting to squeeze that last bit of value out and leveraging the tools they have at hand.

      • 25

        Krasimir Tsonev

        October 17, 2014 7:05 am

        Hi Simone Rescio,

        thanks for the comment. I see your point and I agree with some of the things. However, that was the only solution that I found. I’ll definitely not use that as a primary way of stats collecting.

  13. 26

    Never thought about it but it actually makes much sense. Thanks for sharing the clever idea! Though I should add, I hope this is going to be used only restrained.

  14. 27

    Very interesting and helpful article

  15. 28

    This is worth considering if only to prove that you don’t know how many visitors you’re ignoring who only have feature phones.

    • 29

      Krasimir Tsonev

      October 17, 2014 7:06 am

      Hello Michael,

      sorry, what you mean? The trick is used (served) only to the people in that private network. We knew that they are going to use their personal PCs.

  16. 30

    Forgive my limited knowledge but,

    Does the drawback you mentioned, regarding only being able to track the first click, occur because the :active pseudo class applies the background-image to the element, and the next time they click it doesn’t because the URL is the same and therefore no change occurs?

    If so, couldn’t you set the background-image twice in the :active state, the second one being a neutral used to clear the tracking URL. Would this allow it to apply the tracking image URL each subsequent click?

    • 31

      Krasimir Tsonev

      October 17, 2014 7:07 am

      I know that we tested that, but I’ll ask the team what was the result. It looks logical.

  17. 32

    Mario Goncalves

    October 16, 2014 8:05 pm

    Nice article!

    I aborded this issue a few weeks ago and got to some coincidental conclusions:

    The idea of a middleware is cunning and cool!

    Thanks for the article!

  18. 34

    If you create your tracking part of the css dynamicly, you could easely add a timestamp to the ga image parameter list. That way, you would only get one event per pageload – after the next request, the image url would be different and you would receive another one.

    • 35

      Krasimir Tsonev

      October 17, 2014 7:10 am

      This is indeed clever, but isn’t it require serving a different CSS every time. So, no CSS caching.

  19. 36


    I guess the idea behind this pure CSS approach is to provide at least SOME data from visitors who have JS disabled, right? Because I started wondering how you build the dynamic aspects of the Measurement Protocol call (i.e. the request to /collect). In order for hits to be stitched together in Universal Analytics, you’ll need the clientId to be the same for all hits sent. Otherwise each hit will just create a new session. If you’re hard-coding the query parameters in CSS, does that mean that every single hit will have the same clientId? Also, what about dynamic parameters such as document title, location, path, are those hard coded as well?

    Just wondering. I think this approach has merits in uncovering the scope of traffic that does not use JS (the suggestion above about using .nojs is excellent).

    I’m also very concerned about coupling the presentational layer with data collection, but, like I said, if the point of this exercise is to uncover the size and scope of a specific problem (how many hits come from non-JS enabled browsers), then this is a great patch.

    • 37

      Krasimir Tsonev

      October 17, 2014 7:13 am

      Actually we were interested only in how many times a particular link is clicked (for example). It wasn’t important if the user is the same or not. But yes, you are right. Our use cases is kind of specific one and this technique may not work for other companies.

  20. 38

    Евала :)

  21. 39

    Krasimir Tsonev

    October 17, 2014 7:10 am

    Yeah … that’s really helpful information. One note: considering the fact that we are changing the background of the DOM elements we should make sure that the page looks ok if we load non-transparent gifs.

    • 40

      …or you can use CSS3 multiple backgrounds.

      • 41

        Adam Konieska

        October 17, 2014 4:29 pm

        Except with the extra text needed to for the CSS3 multiple background, you’d be over the 46 bytes you were trying to avoid in the first place!

  22. 42

    Great article!! Very informative. Thank you.

  23. 43

    I’m really impressed by described technique, very creative use of CSS!

  24. 44

    Just want to say: Thank you!
    Great read and might come in handy in the future.


  25. 45

    Robert Broley

    October 17, 2014 9:57 am

    This looks rather interesting. Lets see how it develops over the next few months.

  26. 46


    October 17, 2014 10:22 am

    I may be wrong, but from what I understood in CSS, images are preloaded (means if you have background-image on :active, the image is preloaded when the CSS is loaded).

    If what I say is correct (and I’m not sure), it means that you are going to trigger all “tracking” images at same time, even if the event doesn’t happen.

    Am I wrong? If that’s the case, the entire thing is useless.

    • 47

      Mario Goncalves

      October 17, 2014 10:28 am

      Image tags are preloaded.

      Images in css are onĺy loaded when the selector rule applies . So if they are applied to a conditional selector ( either media queries or pseudo selector ), they will only load once that condition becomes true.

    • 48

      Oliver Cromwell

      October 18, 2014 10:40 pm

      well it is pretty useless yes

  27. 49

    One obligatory note must be that you are relying on an implementation detail of Google Analytics and not on a public API (as far as I know, correct me if I am wrong).
    If they change the beacon URL or the query string parameters or values (and they most certainly can, in case it is indeed an implementation detail), you are basically screwed without any advance notice.

    Using a middleware is better for a few reasons –
    – You can use smaller URLs (that depends on the use case, of course).
    – Everything would go through that and you can change the beacon URL when Google Analytics changes it more easily (less places).
    – You can use cookies in order to create sessions (as long as the browser accepts them, of course).

    • 50

      Krasimir Tsonev

      October 17, 2014 11:36 am

      Good point. The site that we’ve build was a two weeks campaign web app. So, I didn’t hit that problem. But you are right. It is a good idea to have a middleware.

  28. 51

    I can foresee a Sass mixin for this…

  29. 52

    “On one page, we had a step-by-step questionnaire. At the end, the user was asked to agree with some terms and conditions. Some of the visitors did not complete that last step. In the first version of the site, we were not able to determine what these users had selected in the questionnaire because the results would have been sent after completion. However, because all the steps were just radio buttons, we used the :checked pseudo class and successfully tracked the selections:”

    So you collected their input even though they didn’t agree to the T&C’s… Is that even legal?

    • 53

      Krasimir Tsonev

      October 18, 2014 12:14 pm

      It depends of what you add in the T&C. In our cases it was only about the information typed by the users in the very last step. A page that contains their name and company position. In fact we weren’t able to use the described technique to get this data anyway.

  30. 54

    Oliver Cromwell

    October 18, 2014 10:39 pm

    Article is super boring and you didn’t specify why would anyone want to use that method…

  31. 55

    Barış Ünver

    October 19, 2014 10:06 pm

    Awesome article! As for antiviruses and browser settings blocking 1×1 beacons, we can use a tiny bit larger images and they will be the same size with the 1×1 beacon. I got 43 bytes on 1×1, 2×1 and 3×2 images (never saw 42 bytes though).

  32. 56

    Michael Lieberman

    October 21, 2014 8:11 pm

    Another option for tracking behaviours and sending them to Google entirely through the front-end is our service

  33. 57

    This is nice as a proof-of-concept but I don’t think anyone should actually use this in the wild. Besides the drawbacks listed in the article (and these comments), the major one for me is the risk of sending broken data when Google changes its API for Analytics. The JS tracking abstracts away their internal variable names and methods so you don’t need to worry about what order parameters need to come in or what they’re called etc. Doing it this way leaves you open to sending faulty data (or none at all) next time Google updates/changes their code.

    • 58

      Krasimir Tsonev

      October 22, 2014 10:47 pm

      I agree. I discussed the article with a couple of friends and every time I pointed out that this works for us because we had 2 weeks long campaign. Indeed a company that plan to use this technique should know that Google may change their API.

  34. 59

    In the cases where you needed another real background image could you use the ::before or ::after class pseudo-selectors? e.g.

    .checkbox:checked { background: url('foo.jpg'); }
    .checkbox:checked::before {background: url(/* tracker URL */); }

    then you could also use display:none;. This is of course assuming browsers that support ::before and ::after

  35. 60

    I can’t see much need for this since so few people browse without Javascript. Also, as others mentioned, the code would need manually modified if/when Google updates its Analytics API.

    Regardless, it’s a very clever technique. I was hoping for a live example of the parameter string to pass to the pixel.


↑ Back to top