Making Our Own Twitter Timeline



Twitter has grown into a real social phenomenon. This is an incredible achievement for such a simple service. But as you know, great ideas are not necessarily complex.

This time we are going to create our own twitter-like timeline, where you can view and post your tweets. You can use the code I've provided here for all kinds of purposes and be sure that the possibilities are endless. So grab the demo files and start learning!

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

Creating the DB

If you'd like to run a working demo on your own site, you will have to create a MySQL table where all your tweets are going to be stored. You can run the following SQL code through phpMyAdmin (the code is also available in table.sql in the tutorial files):


CREATE TABLE `demo_twitter_timeline` (
  `id` int(10) NOT NULL auto_increment,
  `tweet` varchar(140) collate utf8_unicode_ci NOT NULL default '',
  `dt` datetime NOT NULL default '0000-00-00 00:00:00',
  PRIMARY KEY  (`id`)

The table can be in any mysql database. Just remember to update the data in connect.php.


Thanks to CSS and jQuery, our XHTML code is quite simple. This is what you can see in index.php in the demo files.


<div id="twitter-container">
<form id="tweetForm" action="submit.php" method="post">

<span class="counter">140</span>
<label for="inputField">What are you doing?</label>
<textarea name="inputField" id="inputField" tabindex="1"rows="2" cols="40"></textarea>
<input class="submitButton inact" name="submit" type="submit" value="update" />

<span class="latest"><strong>Latest: </strong><span id="lastTweet"><?=$lastTweet?></span></span>

<div class="clear"></div>

<h3 class="timeline">Timeline</h3>
<ul class="statuses"><?=$timeline?></ul>

Our entire timeline is positioned inside a div container with an id of twitter-container. It has some interesting styles attached to it in the CSS file below.

Next we have the form with an id of tweetForm. This form is submitted via AJAX, so it doesn't really matter what the action and submit attributes are set to.

Inside the form we have a special span element. It acts as the counter, that shows the current number of characters that are filled in the box. As in twitter, the limit here is set to 140 characters.

Later we have the label for the textarea, the textarea itself and the submit button, which is disabled by default (this is done with jQuery and a special CSS class - inact, as you will see later).

After that we have our latest tweet and a clearing div. With that div we address an interesting shortcoming of CSS, as you will see in a minute.

Finally, there is the timeline itself, containing our latest tweets.

Lines 9 and 16 are highlighted to show you that we are displaying PHP variables. We will explain them and generate the list in a moment.

The twitter timeline


We already mentioned that with the use of CSS we are able to drastically reduce the amount of XHTML code we write. An additional bonus is that it is really easy to change the looks of our projects at any time, just by changing the style sheet.

Now lets see what lays in our demo.css file.


/* Page styles */



/* Form & timeline styles */

    -khtml-border-radius: 12px;
    -webkit-border-radius: 12px;

    border:6px solid #f5f5f5;


    font-family:'Lucida Grande',sans-serif;



    margin:5px 0 10px 0;

    border:1px solid #AAAAAA;
    padding: 4px 2px;

    font-family:'Lucida Grande',sans-serif;



    -khtml-border-radius: 6px;
    -webkit-border-radius: 6px;

    border:1px solid #cccccc;
    background:url(img/button_bg.gif) repeat-x #f5f5f5;



    border:1px solid #eeeeee;

    color: #666666;

    margin:10px 0;

ul.statuses li {
    border-bottom:1px dashed #D2DADA;
    padding:15px 15px 15px 10px;

ul.statuses li:first-child{
    border-top:1px dashed #D2DADA;

ul.statuses li:hover {



ul.statuses a img.avatar{
    border:1px solid #446600;

li a, li a:visited {

li a:hover{

We start off by defining the page styles. First we reset our page (nullifying the margin and padding of some of the page elements, which differ by default on the different browsers). After that on line 8 we set a top margin for the body and a font color for all the text on the page.

Lines 16 to 19 is where we round the div, containing our form and timeline. Not until recently, you had to manually create rounded corner graphics and insert additional div elements for each corner. But recent versions of Firefox and Safari can make it with pure CSS.

Unfortunately this method is not supported by other browsers. Another small disadvantage of the technique is that you have to target each browser with browser specific CSS properties - such as -moz-border-radius, because rounded corners are not a part of the current CSS specification. But in case it is included in a future specification, we include the property that should be directly supported - border-radius, which results in the aforementioned 4 lines of code.

The CSS is pretty straightforward up to line 59. This is an important CSS hack (called clearfix) I mentioned earlier. When a div contains floated elements, its height is not enlarged to the height of its children elements. For this purpose another div is inserted which has the CSS poperty clear:both. This forces it to go on a new line, below the floated elements and thus expanding its parent element's height.

Line 63 is where we style our submit button. Here we use the rounded border property again, which also works on buttons as you can see yourself. Another important thing to note is that we define a background graphic for the button. It is exactly twice the height of the button. In its normal state, the top part of the image works as the background, and on hover - the bottom part. This what we do on line 82.

On line 87 is the inact class. This class is assigned to the button only when it is disabled (on the initial page load, or when the text area is empty) to prevent the user from submitting it. It also has a defined normal and :hover state, but they are absolutely the same. This is done to stop the other :hover action, defined on line 81 that affects the button. In other words, we set up a new :hover class to overwrite the previous one.

Lines 102 to 116 define the styles of the timeline elements. The timeline is nothing more than an unordered list. The interesting thing to note here is how we address only the first li element with the :first-child selector and give it a top border.

The jQuery code

Once again I've chosen jQuery because of its agile and simple methods that get more work done with fewer lines of code.



    $('#inputField').bind("blur focus keydown keypress keyup", function(){recount();});





function recount()
    var maxlen=140;
    var current = maxlen-$('#inputField').val().length;

    if(current<0 || current==maxlen)


    else if(current<20)



function tweet()
    var submitData = $('#tweetForm').serialize();

    $('.counter').html('<img style="padding:12px" src="img/ajax_load.gif" alt="loading" width="16" height="16" />');

        type: "POST",
        url: "submit.php",
        data: submitData,
        dataType: "html",
        success: function(msg){

                $('ul.statuses li:first-child').before(msg);





We can divide this code into three important paths. The one that gets executed after the page is loaded (line 1). The recount() function, that fills our counter span with the number of characters left, and the tweet() function that handles the AJAX communication and the subsequent page update to include the new tweet in the timeline.

In the first part, on line 3 you see that we bind the recount() function to a number of events that may happen in the text area. This is because any one of those events by its own cannot guarantee fast enough updates on the counter.

On the next line we disable the submit button - we do not need the user to be able to submit an empty form.

Later we bind the onsubmit event of the form to the tweet() function, preventing the actual form submit on line 9.

In the recount function there are a number of things that are worth mentioning. In lines 17-19 we calculate the remaining characters and on lines 21-36, depending on how close we are to the maximum number, set the color of the counter.

We also decide whether we should disable the button (if there is no text in the text area, or we are over the limit) and enabling it otherwise. The disabling / enabling of the button happens by setting the attribute disabled and assigning our custom CSS class - inact, which removes the hand cursor and changes its color to light grey.

The tweet function is where the magic happens. We serialize the form in the submitData variable and replace the counter with a rotating gif animation.

After this, the data gets sent to submit.php and depending on the return value, inserts the received tweet in the time line on line 55 and 56.

What do actually those two lines of code do? Line 55 uses the same :first-child selector as in our style sheet above. This means that it will insert the formatted tweet it receives before the first element. Then why do we need the second line? Well, if we haven't posted any tweets, the :first-child won't find any elements. That is why we use the :empty selector. Only one of those two lines can insert the element at the same time, thus eliminating the need of checking manually whether there are elements in the timeline.

After inserting our newly created tweet, we empty the text area and recount the remaining characters.


Our PHP code manages the insertion of data in the MySQL database and the formatting of our tweets and timeline.


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


$_POST['inputField'] = mysql_real_escape_string(strip_tags($_POST['inputField']),$link);

if(mb_strlen($_POST['inputField']) < 1 || mb_strlen($_POST['inputField'])>140)

mysql_query("INSERT INTO demo_twitter_timeline SET tweet='".$_POST['inputField']."',dt=NOW()");


echo formatTweet($_POST['inputField'],time());

First we check whether magic_quotes_gpc is set. This is enabled on some hosts and what it does is escape the incoming data automatically, which is considered a bad practice. That is why if it is on, we remove the escaped code and can continue normally with our script.

We escape the data, do a check of the length of $_POST['inputField'] and insert the row in our database. We echo a formatted tweet using formatTweet (more on that in a minute), that is returned to the tweet() jQuery function as the variable msg.


if(!defined('INCLUDE_CHECK')) die('You are not allowed to execute this file directly');

function relativeTime($dt,$precision=2)
    $times=array(   365*24*60*60    => "year",
                30*24*60*60     => "month",
                7*24*60*60      => "week",
                24*60*60        => "day",
                60*60           => "hour",
                60              => "minute",
                1               => "second");


        $output='less than 5 seconds ago';
        foreach($times as $period=>$name)
            if($exit>=$precision || ($exit>0 && $period<60))   break;
            $result = floor($passed/$period);

                $output[]=$result.' '.$name.($result==1?'':'s');

            else if($exit>0) $exit++;

        $output=implode(' and ',$output).' ago';

    return $output;

function formatTweet($tweet,$dt)
    if(is_string($dt)) $dt=strtotime($dt);


    <li><a href="#"><img class="avatar" src="img/avatar.jpg" width="48" height="48" alt="avatar" /></a>
    <div class="tweetTxt">
    <strong><a href="#">demo</a></strong> '. preg_replace('/((?:http|https|ftp):\/\/(?:[A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?[^\s\"\']+)/i','<a href="$1" rel="nofollow" target="blank">$1</a>',$tweet).'
    <div class="date">'.relativeTime($dt).'</div>
    <div class="clear"></div>

Here you can see 2 functions. The first one - relativeTime() is a function I made a while back, that displays the relative period that has passed since a given time (it supports both a unix time stamp as well as a mysql date string as a parameter) .

The other one is made especially for this tutorial. It formats and returns a tweet using only the tweet text and a time variable. If you plan on making changes to the example, this is the place to start.

The formatTweet function is nothing special - first we decide whether we should convert the given time parameter from a mysql data string into a timestamp. After this, we prevent possible vulnerabilities by using htmlspecialchars, and then return a formatted tweet. An interesting thing to note here is line 53. With the preg_replace function we convert the links that are included in the tweet into real hyperlinks, complete with a target and nofollow attribute.

Usage of the functions in the timeline

Now lets see how our timeline is actually generated.



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

// remove tweets older than 1 hour to prevent spam
mysql_query("DELETE FROM demo_twitter_timeline WHERE id>1 AND dt<SUBTIME(NOW(),'0 1:0:0')");

//fetch the timeline
$q = mysql_query("SELECT * FROM demo_twitter_timeline ORDER BY ID DESC");


// fetch the latest tweet
$lastTweet = '';
list($lastTweet) = mysql_fetch_array(mysql_query("SELECT tweet FROM demo_twitter_timeline ORDER BY id DESC LIMIT 1"));

if(!$lastTweet) $lastTweet = "You don't have any tweets yet!";

This code is positioned before any XHTML code on the page. For the purposes of the demo, I've set the tweets to be automatically deleted after an hour. You can remove this line to keep the tweets indefinitely.

The main purpose of this code is to generate the $timeline and $lastTweet variables, that are included in our XHTML code in the beginning of the tutorial.

With this our own twitter timeline is complete.


Today we used PHP, MySQL, jQuery, CSS and XHTML to create our very own twitter-like time line. The possibilities are endless - you can turn this example into a guestbook, a community tweet on your site, a shoutbox or even the next twitter.

Bootstrap Studio

The revolutionary web design tool for creating responsive websites and apps.

Learn more

Related Articles

Blueprint Web Design Blog

thanks for sharing :>

omg thats amazing dude! i might just have to setup one of those for my self

thanks alot for sharing

Thanks for sharing.. Really nice and useful tool

Sam Clarke

Excellent mate,

Thanks a lot!




Mike Brisk

Very cool! Would there be a way to intergrate a login system or twitter login or a retweet button.

Martin Angelov

@mike -
There is always a way. However it would be out of the reach of this tutorial.

will try out soon. Thanks for the cool share. Also, I agree with Mike.

Interesting Script!

Was just looking at this to see the jQuery side of things, noticed though the relativeTime time function doesn't work correctly?

After 5 seconds the script still shows 'less than 5 seconds ago', and when it does show the alternate else statement, i had a post which was posted at 10:00 am and at 11:51 the scripts showing posted 51 minutes ago. Doing the maths on this it would appear its 1 hour and 51 minuntes ago?

Also after 5 seconds the scripts still showing the 5 second message, only when it gets to about 50 minutes it starts to show.

Anything thats wrong with my setup to cause this ??



Martin Angelov

@ Steve

Thanks for the comment, Steve. It sounds like your database is set up to a different time than your PHP configuration. Try executing the following command in MySQL (you can do it via phpMyAdmin):


Then run the following PHP code:

echo date('r');

And compare the two. If this is the case, you might have to add a hour's seconds in line 13 of functions.php.

Hi Martin,

Well spotted my friend!!! I have access to the php.ini file so i changed the timezone in there and it all seems to work fine now.

Thanks for you quick response and acurate answer on this!


Martin, you have done a great job. This is the kind of things I like to learn. You have just encouraged me to learn PHP and JQuery. Thisi s excellent tutorial and I will come back again to apply it and see what I can do to enhance this little tool.

Because when you post a "tweet" it appears that?

Fatal error: Call to undefined function mb_strlen () in / home / www / public_html / submit.php on line 15

Martin Angelov

@ Cristher
I've used mb_strlen() instead of strlen(), because strlen() would return a wrong string length for a string with non-english letters.
Unfortunately, you do not have the extension that provides those functions installed with PHP.

As a solution, you can change the mb_strlen() to strlen() (and remove the 'utf-8' parameter) - this will work fine, at least if you plan to use it only with english speaking users.

MF Rahman

Your tutorials are so cool and extremely useful! Great work!

I was thinking of putting this on a public site for anyone to post but obvouisly there is the age old problem of spam so I was wondering if you could tell me the code for adding an additional hidden input that would grab the posters ip address and put it in the database so I could block it where nessasary ...thank you in advance.

Martin Angelov

@ SomeOne

You'll have to add an additional field to the database table (you can call it ip). After this you just need to change line 13 of submit.php:

mysql_query("INSERT INTO demo_twitter_timeline SET tweet='".$_POST['inputField']."', dt=NOW(), ip='".$_SERVER['REMOTE_ADDR']."'");

This will store the IP of the one posting the tweet in the DB.

Hi, I want to block writing some words. Can you help me how can i do it...

Martin Angelov

@ RfN

At the top of submit PHP you can add these lines:

$block_words = array('word1','word2','word3');

$_POST['inputField'] = str_ireplace($block_words, '*****', $_POST['inputField']);

This will replace all the words in the array with ****.

Martin the code dont work in my site. I dont know why. When I put the code in submit.php all the writings in textbox doesnt write anywhere after i push the submit button.
You can also see it in this link

Know the writing problem is ok but the banned words also writes...

Martin Angelov

Updated the code. It should work now.

:) Its ok now, Thank you very much Martin...

I know i bother you but I have one more question. I done the job but it bans only the english worlds. It can not work in words like ş,ö,ç,ğ. Do I have a chance to fix it...

Martin Angelov

The problem arises from the fact that submit.php is not saved as a UTF8 document.

In your IDE you must have an option to change the document encoding. I can give you an example for Dreamweaver:

Modify -> Page Properties -> Title/Encoding

Then set Encoding: UTF8. After this refill the $block_words array manually.

This should fix your problem.

its ok now. thank you martin...

I'm having what I think is a CSS problem.

As your code stands it is perfect, however I needed to shrink it down to fit in my side bar, approx 400px.

The container has resized as I expected, however the text and textarea part of the form will NOT shift left. It starts approx 33% to the right of the "twitter-container " and overlaps quite a bit over the right hand side.

I've shrunken the textarea but still no dice.... any suggestions?

Also, I've checked all my other css alignments and margins. That all looks good.

Any ideas at all?

Never mind I figured it out.

Thanks for a great tutorial.

Weldone Martin; what a great tutorial and pieces of work

Was trying the demo on my system; when i type the update button was not enabled to enable me submit my tweet!

pls if you can help....

Hello Martin, Twitter/Guestbook like application looks great i tried it, but run into a stop. What ever I type shows up right then and there, but when i revisit the page within the next second no old post show up. I checked the mySQL database and the entries are still in there and the connect.php file has correct information, could u tell me what could da problem be. Thanx

Nevermind Figured it out, the demo Index.php has a typo i'm guessing with :

So i changed it to :

And it worked


Hi Martin i just have problem at the moment i am testing Twitter Time line
on my computer at home but wen i do a post and do a reload current page in Firefox the post is removed from the Twitter Time line can you help me with that please.

Is there a simple way to make an rss feed from this timeline?


Wow really great work man.. Keep it up...

If we add the automatic update of tweet, then it will be much better...

Small function for clickable links and email addresses:

function clickable_link($text)
$ret = ' ' . $text;
$ret = preg_replace("#(^|[\n ])([\w]+?://[\w#$%&~/.-;:=,?@[]+])#is", "\1<a href="\2" rel="nofollow">\2</a>", $ret);
$ret = preg_replace("#(^|[\n ])((www|ftp).[\w#$%&~/.-;:=,?@[]+]
)#is", "\1<a href="http://\\2" rel="nofollow">\2</a>", $ret);
$ret = pregreplace("#(^|[\n ])([a-z0-9&-.]+?)@([\w-]+.([\w-.]+.)*[\w]+)#i", "\1<a href="mailto:\2@\3" rel="nofollow">\2@\3</a>", $ret);
$ret = substr($ret, 1);
return $ret;

hi martin;
i have a problem with spam messages. i want to disable to sent message if the message includes the words which i select firstly. what can i do


below is a simple but really effective function for bad words filter:

$your["keywords"]='the list of your keywords';

$isAllowedKeyword = true;
$KeywordBans = explode("\n", stripslashes($your["keywords"]));
$keyWord = trim($KeywordBans[$i]);
if($keyWord=="" or $keyWord=="\n" or $keyWord=="\r" or $keyWord=="\r\n" or $keyWord=="\t") continue;
if(preg_match("/".$keyWord."/", $_REQUEST["text"])){
$isAllowedKeyword = false;

if ($isAllowedKeyword==false) {
echo 'spam';
} else {
echo 'no spam';

Rahul Chowdhury

Nice tut man, I like this!! Useful!

Hi Martin,

Thank you for sharing with us app your skill development. It is a good way for me to learn and improve. Men are larger when sharing !

Congralutions :)

action & submit attributes??? Should be "action & method attributes, right?


It looks nice!!Thanks for sharing,but I find a problem,if I submit a piece of very long message,it takes a lot of time,and more worse,the update button are still active,so I click the button one more time and...

Great Job,Can you translate this code to the ASP.NET , and C # Code , it will be a useful tools ^^

Thanks for the tutorial, very much thanks... Can I get the tutorial to add twitter style pagination? I could't find one....

Dear Martin,
I have an question about this tutorial. I got warning how can I fix it? I am not good at script but I try many time to change it but is not do a thing at all. A warning is
Warning: mysql_fetch_assoc() expects parameter 1 to be resource, boolean given in C:\xampp\htdocs\Lis-Fashion\comments\index.php on line 25
How can I change its?

regards, YuuJunG


How are you displaying usernames for the different people that post on your timeline? Are you using facebook api, or have you integrated your own login system? The latter is what I am attempting to do now and I need all the help i can get!

Kind Regards


awesome! thanks for sharing... :)

Hey thanks for sharing - i want to use this for a personal project
but i have one question, if much users enters much messages the timeline gets a lil bit long :)
is there a "easy" way to break the timeline after (for example) 10 messages in 2 sites and then 3, 4, 5
hope you know what i mean :)


Fantastic tutorial, thank you for this. Gonna modify it and use it on my own site.

Also, I realized there's an XSS exploit in this. Is there a way to sanitize the "latest tweet" in our AJAX function before we post it?

alert(document.cookie) as a status won't get sanitized before the latest tweet is set as that. This is only a problem for the person setting the status, as the PHP script obviously sanitizes the status before putting it in the timeline.

Oops, the commenting system took out my and tags around that alert(document.cookie)


will be the good update:
login to twitter, to post with my photo, not only anonymously :)

Hey nice work...really appreciated
im a bit of a newbie...
just want to ask about a little problem im facing...whenever a post is written and then i press enter after the post n then submit it...there is always an extra "n" at the end of the post...i wonder why...

Chad Buie

OMG MySQL still running some powerful stuff. I wonder what MYSQL cluster has to offer? Good job! :-)

I don't want the post clear each one hour. PLs where i can change it?

Haapy Xmas to everyone,

I want more tha 140 characteres in the "tweet" where should i change to do do it?