"Who Is Online" Widget With PHP, MySQL & jQuery


For this week's tutorial, we are taking a look at our ever-so-interesting inbox. It all started with a letter from one of our readers a couple of weeks ago:

My boss is always coming into my office asking me to install things on our client's sites. One of the items that's been coming up more and more is "How can we tell who's currently online?" So, the next time you need a tutorial idea, there you go - a php/mysql/jquery 'online users' widget. Oh, and a geomap of each visitor would be super rad too.

Thanks for everything you guys do, Taylor

Taylor, we are always happy when we receive good tutorial ideas, so today we are doing just that - a "Who is online" widget with PHP, MySQL & jQuery. It will display the number of visitors, currently viewing your site, and thanks to Hostip's free IP to location API, it will even be able to detect the country your visitors are from and display it in a slide out panel.

Note: This tutorial is quite old and doesn't work in PHP7 and above. We are keeping it online only as a reference.

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.


<div class="onlineWidget">

<div class="panel">

    <!-- Fetched with AJAX: -->

    <div class="geoRow">
    <div class="flag"><img src="who-is-online/img/famfamfam-countryflags/us.gif" width="16" height="11"></div>
    <div class="country" title="UNITED STATES">UNITED STATES</div>
    <div class="people">2</div>

    <div class="geoRow">
    <div class="flag"><img src="who-is-online/img/famfamfam-countryflags/uk.gif" width="16" height="11"></div>
    <div class="country" title="UNITED KINGDOM">UNITED KINGDOM</div>
    <div class="people">1</div>


<div class="count">8</div>
<div class="label">online</div>
<div class="arrow"></div>

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


    /* Styling the widget and the sliding panel at once */

    border:2px solid #FFFFFF;
    padding:4px 8px;


    /* CSS3 rules for rounded corners, box and text shadows: */


    -moz-box-shadow:0 0 3px #CCCCCC;
    -webkit-box-shadow:0 0 3px #CCCCCC;
    box-shadow:0 0 3px #CCCCCC;

    text-shadow:0 2px 0 white;


.onlineWidget:hover .arrow{
    /* Changing the background image for the green arrow on hover: */
    background-position:bottom center;

    /* The total number of people online div */


    /* The online label */

    padding:7px 0 0 7px;

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

    /* The green arrow on the right */

    background:url(img/arrow.png) no-repeat top center;


    /* The slideout panel */



    /* The rotating gif preloader image */
    margin:10px auto;

    /* The div that contains each country */

    padding:2px 0;

    margin:0 4px;

.country, .people{



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.


require "connect.php";
require "functions.php";

// We don't want web bots altering our stats:
if(is_bot()) die();

$stringIp = $_SERVER['REMOTE_ADDR'];
$intIp = ip2long($stringIp);

// Checking wheter the visitor is already marked as being online:
$inDB = mysql_query("SELECT 1 FROM tz_who_is_online WHERE ip=".$intIp);

    // This user is not in the database, so we must fetch
    // the geoip data and insert it into the online table:

        // A "geoData" cookie has been previously set by the script, so we will use it

        // Always escape any user input, including cookies:
        list($city,$countryName,$countryAbbrev) = explode('|',mysql_real_escape_string(strip_tags($_COOKIE['geoData'])));
        // Making an API call to Hostip:

        $xml = file_get_contents('http://api.hostip.info/?ip='.$stringIp);

        $city = get_tag('gml:name',$xml);
        $city = $city[1];

        $countryName = get_tag('countryName',$xml);
        $countryName = $countryName[0];

        $countryAbbrev = get_tag('countryAbbrev',$xml);
        $countryAbbrev = $countryAbbrev[0];

        // Setting a cookie with the data, which is set to expire in a month:
        setcookie('geoData',$city.'|'.$countryName.'|'.$countryAbbrev, time()+60*60*24*30,'/');

    $countryName = str_replace('(Unknown Country?)','UNKNOWN',$countryName);

    mysql_query("   INSERT INTO tz_who_is_online (ip,city,country,countrycode)
                    VALUES(".$intIp.",'".$city."','".$countryName."', '".$countryAbbrev."')");
    // If the visitor is already online, just update the dt value of the row:
    mysql_query("UPDATE tz_who_is_online SET dt=NOW() WHERE ip=".$intIp);

// Removing entries not updated in the last 10 minutes:
mysql_query("DELETE FROM tz_who_is_online WHERE dt<SUBTIME(NOW(),'0 0:10:0')");

// Counting all the online visitors:
list($totalOnline) = mysql_fetch_array(mysql_query("SELECT COUNT(*) FROM tz_who_is_online"));

// Outputting the number as plain text:
echo $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= 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.


require "connect.php";
require "functions.php";

// We don't want web bots accessing this page:
if(is_bot()) die();

// Selecting the top 15 countries with the most visitors:
$result = mysql_query(" SELECT countryCode,country, COUNT(*) AS total
                        FROM tz_who_is_online
                        GROUP BY countryCode
                        ORDER BY total DESC
                        LIMIT 15");

    echo '
    <div class="geoRow">
        <div class="flag"><img src="who-is-online/img/famfamfam-countryflags/'.strtolower($row['countryCode']).'.gif" width="16" height="11" /></div>
        <div class="country" title="'.htmlspecialchars($row['country']).'">'.$row['country'].'</div>
        <div class="people">'.$row['total'].'</div>

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.


    // This function is executed once the document is loaded

    // Caching the jQuery selectors:
    var count = $('.onlineWidget .count');
    var panel = $('.onlineWidget .panel');
    var timeout;

    // Loading the number of users online into the count div with the load AJAX method:

            // Setting a custom 'open' event on the sliding panel:

            timeout = setTimeout(function(){panel.trigger('open');},500);
            // Custom 'close' event:

            timeout = setTimeout(function(){panel.trigger('close');},500);

    var loaded=false;   // A flag which prevents multiple AJAX calls to geodata.php;

    // Binding functions to custom events:

                // Loading the countries and the flags
                // once the sliding panel is shown:



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.


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.

What do you think? How would you modify this code?

Presenting Bootstrap Studio

a revolutionary tool that developers and designers use to create
beautiful interfaces using the Bootstrap Framework.

Learn more
Web Browser Frame DevKit Box Mouse Cursor

Related Articles

This discussion is closed.

Holy moly!

Haven't studied the code completely, but I would suggest an auto-refresh with jQuery (only the div).



WoW !

I think this is the best site for learning php, you should make a php series tutorials for beginners =)

Peace from Holland

Eric Grint

Really Great Tutorial.. :)

Taylor Hunt

Well written. The .js has more bindings than Shaun White's snowboard garage. Grabbing the code and going to work as we speak. Rock solid. Martin, you have a free beer (or 12) here in Wichita, Ks anytime.

very usable... your tutorial is cool..
idk you make jquery more amazing...thanks

Very nice and useful! Thanks for the Tut! :D

Is there any way to change the direction it slides? Like for example I want it come slide out to the left if I am keeping it in my sidebar.

Great little script as usual :)

This is an awesome article. I like this so much. Thank you.

Chris Horton

Hi, first time I've been on your site. Really well written tutorial and could come in handy for some clients sites. I'll be subscribing to your RSS.


Hey it's not working!!! I didn't change anything in the code, but the data is not written to database. IP is inserted but nothing else?

Thank you for the great comments!

@ apex
Seems your server cannot communicate with the Hostip API. In the demonstration I've used the file_get_contents() PHP function for fetching the Hostip URL.
It is possible that your host disabled file_get_contents from fetching remote URL's. To test if this is the case, create a new php file and put this code inside it:


echo file_get_contents('http://www.google.com');


You should get the contents of the Google homepage printed out. Write here how this goes.

@ Al
Yes, you could make it slide out to the left. However you'll have to replace most of the CSS and add jQuery UI to the page, as jQuery does not provide a function for sliding out content this way .

I have found the problem. It's not in your script it's working perfectly now. I have replaced "file_get_contents" with "curl", it seems that "file_get_contents" is somehow disabled on my server, I don't know why?

Thanks again for this nice script and for interest in helping me!

Florin M

Can you please share the psd for the "who is online" logo from the preview ? Thank you!

Not working!!

Warning: mysql_num_rows() expects parameter 1 to be resource

Replace this $inDB = mysql_query("SELECT 1 FROM tz_who_is_online WHERE ip=".$intIp);


$inDB = mysql_query("SELECT * FROM tz_who_is_online WHERE ip='.$intIp'");

@ apex
Glad that the problem is solved!

@ Florin M
I usually don't share these, but you can download it from here.

@ Zero
You have probably filled wrong MySQL login details and the script is failing to connect to the server.

@ Martin Angelov

No, my conecction is good and the table is ok, but this line in the code generates the error:


but the query
$inDB = mysql_query("SELECT 1 FROM tz_who_is_online WHERE ip=".$intIp);
returns 0 rows, because is the first time i run the page.

maybe some configuration is wrong

try with SELECT *

you should add
if ($countryName == "") { $countryName='UNKNOWN';$countryAbbrev='XX';$city='(Unknown City?)'; }

before inserting data to database, check your database, sometimes it get's filled with empty rows

Florin M

Thank you , your posts are the best i ever seen! And thank you for the psd!

thanks for this great tutorial


I like this widget!! Thank you :)

Borce M.

Useful, but something I won't use on my blog.

One flaw in this processing is that people who go "offline"(not active after 10 minutes) are not updated in the database until someone else comes online. This would result in potentially inflated online numbers.

Kurt Milam

@Angelo, re:
Yes, you could make it slide out to the left. However you’ll have to replace most of the CSS and add jQuery UI to the page, as jQuery does not provide a function for sliding out content this way.

You can do this with straight jQuery no jQuery UI required. Just use the standard animate() function in jQuery to make the div slide to the right or left.

Nice tutorial!

Michael Pehl

Very cool stuff! Nice tut, thanks for sharing this beauty.

Hey Martin !

F*cking great article as usual !
So complete & well explained, THE top of the cream out there...

Love your tuts man. Keep up the good work !

Keerthi Teja

Hello Martin.... :)
As usual Great Tut...
I have a doubt....
I want to use this script in multiple sites....
Means i want to install this script in one site and i want to run this in my other websites...
How to do that...?

I think u understood my question...
Thanx in Advance... :)

u can use iframe code put in it the link to the master page that u put the online code in it.

<iframe src="#"></iframe>


really nice and small widget to use in project, best combination of PHP,Mysql and jquery in love i am.

Thank you for the great comments!

@Keerthi Teja - the script is organized in a way that it is only suitable for use in a single site. You could modify it to work with more than one site (by adding one more field to the database and reorganizing the code), but I would suggest to use it as is and implement it for each site individually.

Hello !
thanks for putting together this nice tutorial.

I was wondering if it's possible to have the results displaying on one page only while recording data from other pages. Let me put it in my website's context :

I have 4 index pages with pull-down menus (one per language) that loads the page contents into an iframe underneath. So will it work if I place code that records visitor into each of the 4 index pages, and the code that display the result in the "stats" page (see left handside box : icon with vertical bars). In addition, I also have a store section based on zencart where I want to record but not display the number of visitors.

To record the visitors online, only the two line pointing to the js files are needed, right ? or the section div class="onlineWidget" as well ?

I hope I explained clearly. Thanks in advance for your answer.
Best regards



Thank you so much, this is awesome!

@ Denis

Currently the widget in the demo shows all of Tutorialzine's online visitors, despite the demo running on a different subdomain than the main site. To make this work, you need to include an image on the page you want to be tracked, which points to online.php.

You can do it either with a regular image tag somewhere in the markup of the page:

<img src="http://domain.com/somepath/who-is-online/online.php" width="1" height="1" />

Or with JavaScript:

<script type="text/javascript"> (new Image()).src="http://domain.com/somepath/who-is-online/online.php"; </script>

You can show the online widget in an entirely different page (or even an admin area) not accessible by regular visitors.

Hey Martin,

thanks for the explanation : I have done as you said and it works like a charm. Widget is up and running on my stats page. Brilliant, thanks a million !!


Michael Pehl

I always see "Unknown". When checking with hostip.info it recognizes my ip correctly, but on my site http://1click.at it always shows "Unknown".

Any tipps on that?

Michael Pehl

Sorted it out, thanks.


1st problem:

Hostip API returnes, for example, for

So, after strtolower($row['countryCode']).'.gif
we get 'uk.gif'

But famfamfam doesn't have this UK flag, it has 'gb.gif'.

So, I see these 2 systems -- Hostip API & famfamfam -- are not compatible.

2nd issue:

ip int(10) NOT NULL default '0',

I think int(10) is too small for ip2long, so MySQL saves an incorrect value for the same

I made it larger as varchar, after that it works correctly with this IP.

3rd (system) issue: Hostip API is very... narrow. For example, it doesn't know my IP (EU country, capital):

> Location: ... actually we haven't a clue.

A very interesting architecture of the solution, but the Hostip API service is very weak. As a result, the countries are mainly 'UNKNOWN'.


1) Both the hostip API and the famfamfam iconset follow the same standard. The difference comes from the fact that there has been a change with the standard for the GB/UK code you mention. By simply renaming the flag (or using the icon pack that is supplied with the demo) you should be OK.

2) As we are using a signed value for the IP address (positive and negative numbers), INT(10) is sufficient. However you are free to change it to a varchar if you like, and drop the ip2long conversion entirely.

3) Yes, hostip is community supported and it does have its pitfalls. However I hope that if it becomes more popular, the accuracy would increase. There are commercial alternatives, which provide a much higher level of accuracy, but again, they are not free. This widget is more for the average webmaster, who is curious about his community, and I think for the most part it gets the job done.


1) If I use this code:

// $stringIp = $_SERVER['REMOTE_ADDR'];
$stringIp = '';
$intIp = ip2long($stringIp);
echo $intIp;

& INT(10) in MySQL

I see:
1) 1579063396 in ECHO and
2) 1579063396 in MySQL


2) Now I replace

$stringIp = '';


$stringIp = '';

(one line only!)

and I get:
1) 3651741440 in ECHO and
2) 2147483647 in MySQL

3651741440 vs. 2147483647. No idea why.

3) If change IP back:

$stringIp = '';

I get 1579063396 and 1579063396.

Ok again.

So, I don't understand why MySQL saves 2147483647, not 3651741440.

That is why I replaced INT(10) with VARCHAR.

P.S. I'm not a programmer (a hobby).


I've just found.

CREATE TABLE tz_who_is_online (
ip int(10) NOT NULL default '0',

in table.sql (demo files) should be

CREATE TABLE tz_who_is_online (
wio_ip int(10) unsigned NOT NULL default '0',


a correction: without the prefix 'wio_' -- I copied it from my table.

What operating system are you running PHP on? It seems that in your configuration ip2long returns an unsigned number (in PHP on Linux ip2long('') echoes a signed negative number).

This is probably the reason for the problems you are facing. Converting the ip field to INT(11) (or to a string) should indeed resolve your problems. I'm going to update table.sql with the new field, in case anybody else is having the same issues.


Linux, Apache 2, PHP 5.2.12


> [I]Converting the ip field to INT(11)[/I]

I don't know why, but I see INT(11) or INT (12) doesn't solve the problem.
IP is saved correctly only if INT(11) and INT (12) are unsigned.

Thanks a lot. I was trying to implement this but didn't know how to. SO, once again thnx a lot :)


Hi Martin!
Really a good script you are using.
A simple question to you:
How can I decorative items to the Info window comes down, instead it will be Up.

Thank you for the answer


Nevermind...i find it....
in style.css change
/ The slideout panel /




Thanks Angelov

yeah.. really cool script.



To Whom It May Concern,

I am encountering the following problems when I run the demo.html. Can anyone tell me what is wrong with the script? I have posted here previously but it seems there isn't any response.

Warning: mysql_fetch_assoc(): supplied argument is not a valid MySQL result resource in /home/weilik/webapps/wp/who-is-online/geodata.php on line 16

Warning: mysql_num_rows(): supplied argument is not a valid MySQL result resource in /home/weilik/webapps/wp/who-is-online/online.php on line 17

Warning: Cannot modify header information - headers already sent by (output started at /home/weilik/webapps/wp/who-is-online/online.php:17) in /home/weilik/webapps/wp/who-is-online/online.php on line 45

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /home/weilik/webapps/wp/who-is-online/online.php on line 72

Please help. Thank you.



Wow what a awesome script. Thanks so much. Got to try this one.

wooow...thanks so much!!!

mp3 dinle

really cool script.


Im lost here. Is there a way to grab this or just get html code to encorporate this into my blog?

Hi Martin!
Really a good script you are using.
A simple question to you:
How can I decorative items to the Info window comes down, instead it will be Up.

Rafael Mussi

I think you should add the timestamp on the first insert query:

mysql_query("INSERT INTO tz_who_is_online (ip,city,country,countrycode,dt) VALUES (".$intIp.",'".$city."','".$countryName."', '".$countryAbbrev."', NOW())");

Btw, nice script!

Amazing tutorial, I'll use this widget on my sites.

I found tutorialzine.com, just today and I love it!

Cool script, but on a heavy traffic sit will kill your database server instantly!

oyun oyna

really cool script.


Axinte Adrian

and this one is really good!
I have 2 use it ;)

No other words to explain about this tutorial... your company always rocks... am the great fan of your website.... keep it up....

script indir

really cool script.


That is really cool!

thanks for the great tutorial :)

Bolarinwa Olakunle

This is a lovely Tut!. Thank You so much

Thanks for sharing that widget, when I tried it on my xampp it worked perfectly but on my web server it didn't.

So I came up with following fix, what is actually working on more configurations then with ip2long because of 32bit and 64bit system.

You can let MySQL do the converting with INET_ATON():

So what you have to change in online.php

$stringIp = $_SERVER['REMOTE_ADDR'];
$intIp = ip2long($stringIp);

$stringIp = $_SERVER['REMOTE_ADDR'];

1 FROM tz_who_is_online WHERE ip=".$intIp

1 FROM tz_who_is_online WHERE ip = INET_ATON(".$stringIp.")"



dt=NOW() WHERE ip=".$intIp

dt=NOW() WHERE ip = INET_ATON(".$stringIp.")"

Again, thanks for sharing, I made a plugin for JAKCMS.

Brett Widmann

This was an awesome tutorial! I really like how it works.

Getting 'UNKNOWN' for IP,etc., here I'm afraid. Searched for this script after seeing something similar elsewhere so would love to get it working. Tried the 'file_get_contents' test with Google and it worked, returning the Google page (text only, no images) so unsure what to test next.

Many thanks in advance.

I also make ip to unsigned Int that solve the problem.
Thnks again

Hey Martin,

What is the function : get_tag() ?

Nice demo buddy!


This is working fine but it is inserting uknown country into the databse and therefore not showing the when i put pointer over the widget. Any help please.


I'm having some problems with this script. It was working fine a few days ago, but after some recent database changes it doesnt work anymore. I have my database details entered correctly, but the slideout is blank and when i go directly to "who-is-online/online.php", the page is blank.

Any idea what's happening?

Ravindar Dev

may be am asking too much but i really need a helping hand here.iam trying to put this plugin exactly at right center of the screen but i couldn't.

a little help is appreciated...

Very nice tutorial.

Why is unique on IP??? What happen when there are more then one users on the same IP???

you should insert command WHERE userid not WHERE IP...(in case you manage that after login script) ...

Hi guys. I wish to use this script on a weebly site. I know weebly doesn't support php/db. I was thinking about setting this script up on a different host and somehow link it to weebly site. Is this possible, is so, can some one guide me on how I can write this up? Thanks.

Is it possible to make the sliding panel to drop down instead of floating up? If so, can someone guide me on how to accomplish this? Thanks.

I am also getting unknown for the ip address. I've seen a couple more posts like me but no answers. Anyone knows the reason why?

Here is a solution to replace hover event by click event:

function open (){panel.trigger('open');}
function close(){panel.trigger('close');}

    var modal_id = $(this).attr('name');


Yes, how i can "refresh" automatic the visitors div?

Again, great tutor, you are the best. Thank you.

Tanas Alexandru Florin

Yes, I want to know how to make this script much faster (or the solution from Linces with refresh div or class)...I'm try use this script on my chat for the online users,but ts very slow and becomes a craziness :)
I want users to be calculated every half minute,its possible ??

Thanks Martin and keep up the good work.

Tanas Alexandru Florin


Hello, first I wanted to congratulate the tutorial, the best on the subject I found :)

I was already using it a few times but today we are in php7 and the standard mysql already no longer works in most applications, it would be nice to give you an updated using mysqli or else the PDO