Step 1 – XHTML
As usual, we start off with the XHTML part. The code presented here might not look like much, but it is all that we need to show off all the work that has been done by the backend. The widget features a slick slide-out panel with all the geolocation data, shown on mouse over.
demo.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<div <span class="hljs-class"><span class="hljs-keyword">class</span></span>=<span class="hljs-string">"onlineWidget"</span>> <span class="xml"><span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"panel"</span>></span> <span class="hljs-comment"><!-- Fetched with AJAX: --></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"geoRow"</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flag"</span>></span><span class="hljs-tag"><<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"who-is-online/img/famfamfam-countryflags/us.gif"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"16"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"11"</span>></span><span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"country"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"UNITED STATES"</span>></span>UNITED STATES<span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"people"</span>></span>2<span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"geoRow"</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flag"</span>></span><span class="hljs-tag"><<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"who-is-online/img/famfamfam-countryflags/uk.gif"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"16"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"11"</span>></span><span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"country"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"UNITED KINGDOM"</span>></span>UNITED KINGDOM<span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"people"</span>></span>1<span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"count"</span>></span>8<span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"label"</span>></span>online<span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"arrow"</span>></span><span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span></span> |
As you may see from the markup above, the main container div – “onlineWidget” contains the slide-out panel (the div with class name “panel“), the total number of people online (the “count” div), the “online” label and the green arrow to the right.

The panel div is dynamically filled by AJAX with the countries with the most visitors currently online. The default content of this div is a rotating gif preloader, which is replaced with the geo data once the AJAX request is complete (usually in less than a second). We will come back to this in a moment.
Step 2 – The Database
Unlike the usual routine, here we are going to take a look at how the database is structured, as it is fundamental to the rest of the script.
All of the widget data is stored into the tz_who_is_online table. It consists of six fields (or columns). The first one – ID, is a standard primary key / auto increment field. After this is the IP field which stores the visitor’s IP address (converted to a integer beforehand with the ip2long PHP function).
After this are three fields fetched by Hostip’s API – Country, CountryCode and City. The widget is not using the city field at this point, but it is good to have in case somebody wants to implement it. Last is the DT timestamp field, which is updated on every page load and enables us to track who is online (users without a page load in the last 10 minutes have probably left the site).

Step 3 – CSS
The widget is (almost) image free, and is only styled with CSS. Lets take a look at the styling, as defined in styles.css. The code is divided in two parts, so it is easier to follow.
who-is-online/styles.css – Part 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<span class="hljs-selector-class">.onlineWidget</span>,<span class="hljs-selector-class">.panel</span>{ <span class="hljs-comment">/* Styling the widget and the sliding panel at once */</span> <span class="hljs-attribute">background-color</span>:<span class="hljs-number">#F9F9F9</span>; <span class="hljs-attribute">border</span>:<span class="hljs-number">2px</span> solid <span class="hljs-number">#FFFFFF</span>; <span class="hljs-attribute">height</span>:<span class="hljs-number">25px</span>; <span class="hljs-attribute">padding</span>:<span class="hljs-number">4px</span> <span class="hljs-number">8px</span>; <span class="hljs-attribute">position</span>:relative; <span class="hljs-attribute">width</span>:<span class="hljs-number">130px</span>; <span class="hljs-attribute">cursor</span>:pointer; <span class="hljs-comment">/* CSS3 rules for rounded corners, box and text shadows: */</span> <span class="hljs-attribute">-moz-border-radius</span>:<span class="hljs-number">6px</span>; <span class="hljs-attribute">-webkit-border-radius</span>:<span class="hljs-number">6px</span>; <span class="hljs-attribute">border-radius</span>:<span class="hljs-number">6px</span>; <span class="hljs-attribute">-moz-box-shadow</span>:<span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">3px</span> <span class="hljs-number">#CCCCCC</span>; <span class="hljs-attribute">-webkit-box-shadow</span>:<span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">3px</span> <span class="hljs-number">#CCCCCC</span>; <span class="hljs-attribute">box-shadow</span>:<span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">3px</span> <span class="hljs-number">#CCCCCC</span>; <span class="hljs-attribute">text-shadow</span>:<span class="hljs-number">0</span> <span class="hljs-number">2px</span> <span class="hljs-number">0</span> white; } <span class="hljs-selector-class">.onlineWidget</span><span class="hljs-selector-pseudo">:hover</span>{ <span class="hljs-attribute">background-color</span>:<span class="hljs-number">#fcfcfc</span>; } <span class="hljs-selector-class">.onlineWidget</span><span class="hljs-selector-pseudo">:hover</span> <span class="hljs-selector-class">.arrow</span>{ <span class="hljs-comment">/* Changing the background image for the green arrow on hover: */</span> <span class="hljs-attribute">background-position</span>:bottom center; } <span class="hljs-selector-class">.count</span>{ <span class="hljs-comment">/* The total number of people online div */</span> <span class="hljs-attribute">color</span>:<span class="hljs-number">#777777</span>; <span class="hljs-attribute">float</span>:left; <span class="hljs-attribute">font-size</span>:<span class="hljs-number">26px</span>; <span class="hljs-attribute">font-weight</span>:bold; <span class="hljs-attribute">margin-top</span>:-<span class="hljs-number">3px</span>; <span class="hljs-attribute">text-align</span>:center; <span class="hljs-attribute">width</span>:<span class="hljs-number">30px</span>; } <span class="hljs-selector-class">.label</span>{ <span class="hljs-comment">/* The online label */</span> <span class="hljs-attribute">float</span>:left; <span class="hljs-attribute">font-size</span>:<span class="hljs-number">10px</span>; <span class="hljs-attribute">padding</span>:<span class="hljs-number">7px</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">7px</span>; <span class="hljs-attribute">text-transform</span>:uppercase; } |
In the first step above, you can see that we style the widget and the slide-out panel at once. This is to ensure that they have consistent styling which is easy to change later on. Some rules are unique to the panel, however, so we include a individually targeted set of rules in the second part of the code.
We also define the hover state and style the label and count divs.
who-is-online/styles.css – Part 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
<span class="hljs-selector-class">.arrow</span>{ <span class="hljs-comment">/* The green arrow on the right */</span> <span class="hljs-attribute">background</span>:<span class="hljs-built_in">url</span>(img/arrow.png) no-repeat top center; <span class="hljs-attribute">position</span>:absolute; <span class="hljs-attribute">right</span>:<span class="hljs-number">6px</span>; <span class="hljs-attribute">width</span>:<span class="hljs-number">25px</span>; <span class="hljs-attribute">height</span>:<span class="hljs-number">25px</span>; } <span class="hljs-selector-class">.panel</span>{ <span class="hljs-comment">/* The slideout panel */</span> <span class="hljs-attribute">position</span>:absolute; <span class="hljs-attribute">cursor</span>:default; <span class="hljs-attribute">bottom</span>:<span class="hljs-number">50px</span>; <span class="hljs-attribute">left</span>:<span class="hljs-number">0</span>; <span class="hljs-attribute">height</span>:auto; <span class="hljs-attribute">display</span>:none; <span class="hljs-attribute">margin</span>:-<span class="hljs-number">2px</span>; <span class="hljs-attribute">z-index</span>:<span class="hljs-number">1000</span>; } <span class="hljs-selector-class">.preloader</span>{ <span class="hljs-comment">/* The rotating gif preloader image */</span> <span class="hljs-attribute">display</span>:block; <span class="hljs-attribute">margin</span>:<span class="hljs-number">10px</span> auto; } <span class="hljs-selector-class">.geoRow</span>{ <span class="hljs-comment">/* The div that contains each country */</span> <span class="hljs-attribute">height</span>:<span class="hljs-number">16px</span>; <span class="hljs-attribute">overflow</span>:hidden; <span class="hljs-attribute">padding</span>:<span class="hljs-number">2px</span> <span class="hljs-number">0</span>; } <span class="hljs-selector-class">.flag</span>{ <span class="hljs-attribute">float</span>:left; <span class="hljs-attribute">margin</span>:<span class="hljs-number">0</span> <span class="hljs-number">4px</span>; } <span class="hljs-selector-class">.country</span>, <span class="hljs-selector-class">.people</span>{ <span class="hljs-attribute">float</span>:left; <span class="hljs-attribute">font-size</span>:<span class="hljs-number">10px</span>; <span class="hljs-attribute">padding</span>:<span class="hljs-number">2px</span>; } <span class="hljs-selector-class">.country</span>{ <span class="hljs-attribute">width</span>:<span class="hljs-number">85px</span>; <span class="hljs-attribute">overflow</span>:hidden; } <span class="hljs-selector-class">.people</span>{ <span class="hljs-attribute">font-weight</span>:bold; } |
In the second part of the file, we style the how the geolocation data that is presented in the slide-out panel, after jQuery fetches it from the back-end. With this we can continue with the next step.

Step 4 – PHP
This is where the magic happens. PHP has to keep the database of online users up to date and fetch IP-to-location data from Hostip’s API. This is later cached for future use in a cookie on the visitors PC.
who-is-online/online.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
<span class="hljs-keyword">require</span> <span class="hljs-string">"connect.php"</span>; <span class="hljs-keyword">require</span> <span class="hljs-string">"functions.php"</span>; <span class="hljs-comment">// We don't want web bots altering our stats:</span> <span class="hljs-keyword">if</span>(is_bot()) <span class="hljs-keyword">die</span>(); $stringIp = $_SERVER[<span class="hljs-string">'REMOTE_ADDR'</span>]; $intIp = ip2long($stringIp); <span class="hljs-comment">// Checking wheter the visitor is already marked as being online:</span> $inDB = mysql_query(<span class="hljs-string">"SELECT 1 FROM tz_who_is_online WHERE ip="</span>.$intIp); <span class="hljs-keyword">if</span>(!mysql_num_rows($inDB)) { <span class="hljs-comment">// This user is not in the database, so we must fetch</span> <span class="hljs-comment">// the geoip data and insert it into the online table:</span> <span class="hljs-keyword">if</span>($_COOKIE[<span class="hljs-string">'geoData'</span>]) { <span class="hljs-comment">// A "geoData" cookie has been previously set by the script, so we will use it</span> <span class="hljs-comment">// Always escape any user input, including cookies:</span> <span class="hljs-keyword">list</span>($city,$countryName,$countryAbbrev) = explode(<span class="hljs-string">'|'</span>,mysql_real_escape_string(strip_tags($_COOKIE[<span class="hljs-string">'geoData'</span>]))); } <span class="hljs-keyword">else</span> { <span class="hljs-comment">// Making an API call to Hostip:</span> $xml = file_get_contents(<span class="hljs-string">'http://api.hostip.info/?ip='</span>.$stringIp); $city = get_tag(<span class="hljs-string">'gml:name'</span>,$xml); $city = $city[<span class="hljs-number">1</span>]; $countryName = get_tag(<span class="hljs-string">'countryName'</span>,$xml); $countryName = $countryName[<span class="hljs-number">0</span>]; $countryAbbrev = get_tag(<span class="hljs-string">'countryAbbrev'</span>,$xml); $countryAbbrev = $countryAbbrev[<span class="hljs-number">0</span>]; <span class="hljs-comment">// Setting a cookie with the data, which is set to expire in a month:</span> setcookie(<span class="hljs-string">'geoData'</span>,$city.<span class="hljs-string">'|'</span>.$countryName.<span class="hljs-string">'|'</span>.$countryAbbrev, time()+<span class="hljs-number">60</span>*<span class="hljs-number">60</span>*<span class="hljs-number">24</span>*<span class="hljs-number">30</span>,<span class="hljs-string">'/'</span>); } $countryName = str_replace(<span class="hljs-string">'(Unknown Country?)'</span>,<span class="hljs-string">'UNKNOWN'</span>,$countryName); mysql_query(<span class="hljs-string">" INSERT INTO tz_who_is_online (ip,city,country,countrycode) VALUES("</span>.$intIp.<span class="hljs-string">",'"</span>.$city.<span class="hljs-string">"','"</span>.$countryName.<span class="hljs-string">"', '"</span>.$countryAbbrev.<span class="hljs-string">"')"</span>); } <span class="hljs-keyword">else</span> { <span class="hljs-comment">// If the visitor is already online, just update the dt value of the row:</span> mysql_query(<span class="hljs-string">"UPDATE tz_who_is_online SET dt=NOW() WHERE ip="</span>.$intIp); } <span class="hljs-comment">// Removing entries not updated in the last 10 minutes:</span> mysql_query(<span class="hljs-string">"DELETE FROM tz_who_is_online WHERE dt<SUBTIME(NOW(),'0 0:10:0')"</span>); <span class="hljs-comment">// Counting all the online visitors:</span> <span class="hljs-keyword">list</span>($totalOnline) = mysql_fetch_array(mysql_query(<span class="hljs-string">"SELECT COUNT(*) FROM tz_who_is_online"</span>)); <span class="hljs-comment">// Outputting the number as plain text:</span> <span class="hljs-keyword">echo</span> $totalOnline; |
This PHP script is initially called by jQuery in order to populate the count div with the current number of people online. Behind the scenes, however, this script writes the visitor’s IP to the database and resolves their IP-to-location data.
This is the best strategy in organizing the back-end, as we keep the calls to the API (which are quite time expensive) distributed to each user as they visit the site for the first time.
The other alternative would be to store only the IPs of the visitors and queue the geolocation data once the panel is shown. This would mean to resolve a huge number of IPs simultaneously, which would make the script unresponsive and get us black-listed from the API. Totally not cool.
You can queue the Hostip’s API by opening a connection to a URL similar to this: http://api.hostip.info/?ip=128.128.128.128. It returns a valid XML response, which contains all sorts of data, including a country and city name associated with the IP, country abbreviation and even absolute coordinates. We are fetching this data with the PHP file_get_contents() function and extracting the bits of information we need.
who-is-online/geodata.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<span class="hljs-keyword">require</span> <span class="hljs-string">"connect.php"</span>; <span class="hljs-keyword">require</span> <span class="hljs-string">"functions.php"</span>; <span class="hljs-comment">// We don't want web bots accessing this page:</span> <span class="hljs-keyword">if</span>(is_bot()) <span class="hljs-keyword">die</span>(); <span class="hljs-comment">// Selecting the top 15 countries with the most visitors:</span> $result = mysql_query(<span class="hljs-string">" SELECT countryCode,country, COUNT(*) AS total FROM tz_who_is_online GROUP BY countryCode ORDER BY total DESC LIMIT 15"</span>); <span class="hljs-keyword">while</span>($row=mysql_fetch_assoc($result)) { <span class="hljs-keyword">echo</span> <span class="hljs-string">' <div class="geoRow"> <div class="flag"><img src="who-is-online/img/famfamfam-countryflags/'</span>.strtolower($row[<span class="hljs-string">'countryCode'</span>]).<span class="hljs-string">'.gif" width="16" height="11" /></div> <div class="country" title="'</span>.htmlspecialchars($row[<span class="hljs-string">'country'</span>]).<span class="hljs-string">'">'</span>.$row[<span class="hljs-string">'country'</span>].<span class="hljs-string">'</div> <div class="people">'</span>.$row[<span class="hljs-string">'total'</span>].<span class="hljs-string">'</div> </div> '</span>; } |
Geodata.php is fetched by jQuery to populate the slide-out panel with location data. This file basically queues the database with a GROUP BY query, which groups the individual users by country and orders the resulting rows in a descending order, with the most popular countries at the top.
For the flag icons, we are using the famfamfam flag icon set, which is released as public domain. A great thing about the Hostip API, is that it returns the country code in a standard two letter format, which is also shared by the famfamfam icon set. This means that in the while loop, it is really easy to find the appropriate flag to show, by just lowering the case of the country abbreviation stored in the database and adding a gif extension.
Step 5 – jQuery
JavaScript manages the AJAX requests and slides the panel. This would be a daunting task with pure JS alone, which is why we are using the newest version of the jQuery library.
Now lets take a look at what the code looks like.
who-is-online/widget.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
$(<span class="hljs-built_in">document</span>).ready(<span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{ <span class="hljs-comment">// This function is executed once the document is loaded</span> <span class="hljs-comment">// Caching the jQuery selectors:</span> <span class="hljs-keyword">var</span> count = $(<span class="hljs-string">'.onlineWidget .count'</span>); <span class="hljs-keyword">var</span> panel = $(<span class="hljs-string">'.onlineWidget .panel'</span>); <span class="hljs-keyword">var</span> timeout; <span class="hljs-comment">// Loading the number of users online into the count div with the load AJAX method:</span> count.load(<span class="hljs-string">'who-is-online/online.php'</span>); $(<span class="hljs-string">'.onlineWidget'</span>).hover( <span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{ <span class="hljs-comment">// Setting a custom 'open' event on the sliding panel:</span> clearTimeout(timeout); timeout = setTimeout(<span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{panel.trigger(<span class="hljs-string">'open'</span>);},<span class="hljs-number">500</span>); }, <span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{ <span class="hljs-comment">// Custom 'close' event:</span> clearTimeout(timeout); timeout = setTimeout(<span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{panel.trigger(<span class="hljs-string">'close'</span>);},<span class="hljs-number">500</span>); } ); <span class="hljs-keyword">var</span> loaded=<span class="hljs-literal">false</span>; <span class="hljs-comment">// A flag which prevents multiple AJAX calls to geodata.php;</span> <span class="hljs-comment">// Binding functions to custom events:</span> panel.bind(<span class="hljs-string">'open'</span>,<span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{ panel.slideDown(<span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{ <span class="hljs-keyword">if</span>(!loaded) { <span class="hljs-comment">// Loading the countries and the flags</span> <span class="hljs-comment">// once the sliding panel is shown:</span> panel.load(<span class="hljs-string">'who-is-online/geodata.php'</span>); loaded=<span class="hljs-literal">true</span>; } }); }).bind(<span class="hljs-string">'close'</span>,<span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{ panel.slideUp(); }); }); |
You might be a bit perplexed with the use of setTimeout in the menu. This is done, so we have a bit of delay between the hovering of the mouse and the actual opening of the slide-out panel. This way, unintentional movements of the mouse cursor over the widget won’t fire the open event, and once opened, will not close it immediately once the mouse leaves it.
With this our widget is ready!
Setting up the demo
At this point you probably want to grab the widget and put it on your site. To make it work, you need to execute the SQL code found in table.sql in the download archive. It will create the tz_who_is_online table in your database, which is used by the widget. Later you need to upload the files to your server and include widget.js to the head section of your page (along with the jQuery library). After this you have to fill your MySQL login details in connect.php and finally add the markup from demo.html to your web page.
Conclusion
Having access to real time data on your site userbase is a dream to any webmaster. Tools like Google Analytics give a great perspective on your site’s reach, but lack the real time feel a simple widget like this can provide.