CSS-Only Solution For UI Tracking

About The Author

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 … More about Krasimir ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

CSS is usually considered a language for applying styles to webpages. However, in this article Krasimir Tsonev will show you that it is more than that. It is also a handy tool for collecting statistics. 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 your project is successful. Google Analytics is a powerful way to collect data. In this article, you will see a CSS-only approach for tracking UI interactions using Google Analytics.

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 Analytics. 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.

The Problem

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.

The Solution

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

Tracking UI with CSS
(View large version)

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 Overview. 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: beacon. 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 said, 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('https://www.google-analytics.com/collect?v=1&_v=j23&a=...');

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

Tracking User Actions

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('https://www.google-analytics.com/collect?v=1&_v=j23&a=...');

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('https://www.google-analytics.com/collect?v=1&_v=j23&a=...');

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('https://www.google-analytics.com/collect?v=1&_v=j23&a=...');

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('https://www.google-analytics.com/collect?v=1&_v=j23&a=...');

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


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('https://www.google-analytics.com/collect?v=1&_v=j23&a=...');

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 CSS
(View large version)

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.


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.

Further Reading

Smashing Editorial (ds, il, og, mrn)