Dynamic FAQ Section w/ jQuery, YQL & Google Docs

Demo Download

In this tutorial, we are making a dynamic FAQ section. The script, with the help of jQuery & YQL, will pull the contents of a shared spreadsheet in your Google Docs account, and use the data to populate the FAQ section with questions and answers.

The best aspect of this solution, is that you can change the contents of the FAQ section from within Google Docs - just edit the spreadsheet. You can even leverage the rest of Google Docs' features, such as collaborative editing. This way, a small team can support the FAQ section without the need of a dedicated CMS solution.

Thanks to Chris Ivarson for designing the Google Docs icon, used in the featured illustration of this tutorial.

Google Docs

Before starting work on the FAQ section, we first need to create a new Google Docs Spreadsheet. As you probably already have an account with Google (if not, create one), we'll move straight to the interesting part.

In a blank spreadsheet, start filling in two columns of data. The first column should contain the questions, and the second one the answers, that are going to become entries in your FAQ section. Each FAQ goes on a separate line. You can see the one that I created here.

After this, click Share > Publish as webpage and choose CSV from the dropdown list. This will generate a link to your spreadsheet in the form of a regular CSV file, which we will use later.


The first step in developing the script is the markup. The #page div is the main container element. It is the only div with an explicit width. It is also centered in the middle of the page with a margin:auto, as you will see in the CSS part of the tut.


<!DOCTYPE html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<link rel="stylesheet" type="text/css" href="styles.css" />



<div id="page">

    <div id="headingSection">
        <h1>Frequently Asked Questions</h1>
        <a class="button expand" href="#">Expand</a>

    <div id="faqSection">
        <!-- The FAQs are inserted here -->


<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="script.js"></script>

The stylesheet is included in the head of the document, and the jQuery library and our script.js are included at the bottom. All of these are discussed in detail in the next sections of this tutorial.

The #headingSection contains the h1 heading, and the expand/collapse button. After it comes the #faqSection div, where the FAQ entries are inserted after jQuery fetched the contents of your Google Docs Spreadsheet.

The FAQ entries are organized as a definition list structure (dl). This is one of the least used HTML elements, but is perfect for our task. Here is how it looks once jQuery adds it to the page.


    <dt><span class="icon"></span>How does this FAQ section work?</dt>
    <dd>With the help of jQuery and YQL, this script pulls the latest data ..</dd>

    <dt><span class="icon"></span>Can you modify it?</dt>
    <dd>This is the best part of it - you can change the contents ..</dd>

The dl element holds a dt for each question and a dd for each answer. The dd elements are hidden with display:none, and are only shown with a slideDown animation once the respective dt is clicked.



The styles, (held in styles.css) are pretty straightforward and self-explanatory. As mentioned above, only the #page div, which acts as the main container, is explicitly assigned a width. It is also centered in the middle of the page with an auto value for the left/right margins.

styles.css - Part 1

    margin:50px auto;

    border:1px solid #8b9ba7;

    background:url('img/faq_bg.jpg') repeat-y #fff;
    padding:20px 90px 60px 60px;
    border:1px solid white;
    text-shadow:1px 1px 0 white;


/* The expand / collapse button */

    background:url('img/buttons.png') no-repeat;
    border:none !important;

a.button.expand:hover{ background-position:0 -38px;}
a.button.collapse{ background-position:0 -76px;}
a.button.collapse:hover{ background-position:0 bottom;}

We are using a single anchor tag for both the expand and the collapse button, by assigning it either the expand or the collapse CSS class. These classes determine which parts of the background image are offset into view. The background image itself is four times the height of the button, and contains a normal and a hover state for both the expand and collapse button versions.

styles.css - Part 2

/* Definition Lists */

    border:1px solid transparent;

dt:hover{ color:#5f6a73;}

dt .icon{
    background:url('img/bullets.png') no-repeat;

dt.opened .icon{ background-position:left bottom;}

    padding:20px 0 0 25px;

When a definition title (dt) is clicked, its respective dd is expanded into view (as you will see in the next section). With this, the dt is also assigned an opened class. This class helps jQuery determine which FAQ's are opened, and in the same time affects the styling of the small bullet on the left of the title.


The jQuery

Moving to probably the most interesting part of the tutorial. If you've been following the tutorials on this site, you've probably noticed that YQL finds its way into a lot of the tutorials here. The main reason behind this, is that YQL makes possible for developers to use it as a proxy for a wide range of APIs, and implement a diverse functionality entirely in JavaScript.

Today we are using YQL to fetch our Google Spreadsheet as CSV and parse it, so that it is available as a regular JSON object. This way, we end up with a free and easy to update data storage for our simple app.



    // The published URL of your Google Docs spreadsheet as CSV:
    var csvURL = 'https://spreadsheets.google.com/pub?key='+

    // The YQL address:
    var yqlURL =    "http://query.yahooapis.com/v1/public/yql?q="+


        var dl = $('<dl>');

        // Looping through all the entries in the CSV file:

            // Sometimes the entries are surrounded by double quotes. This is why
            // we strip them first with the replace method:

            var answer = this.answer.replace(/""/g,'"').replace(/^"|"$/g,'');
            var question = this.question.replace(/""/g,'"').replace(/^"|"$/g,'');

            // Formatting the FAQ as a definition list: dt for the question
            // and a dd for the answer.

            dl.append('<dt><span class="icon"></span>'+

        // Appending the definition list:

            var dd = $(this).next();

            // If the title is clicked and the dd is not currently animated,
            // start an animation with the slideToggle() method.




            // To expand/collapse all of the FAQs simultaneously,
            // just trigger the click event on the DTs

            else $('dt:not(.opened)').click();

            $(this).toggleClass('expand collapse');

            return false;


It may not be clear from the code above, but jQuery sends a JSONP request to YQL's servers with the following YQL query:

WHERE url='https://spreadsheets.google.com/...'
AND columns='question,answer'

CSV is a YQL table, which takes the URL of a csv file and a list of column names. It returns a JSON object with the column names as its properties. The script then filters them (stripping off unnecessary double quotes) and inserts them as a definition list (DL) into the page.

With this our dynamic FAQ section is complete!


To use this script with your own spreadsheet, you only need to edit the csvURL variable in script.js, and replace it with the CSV URL of your spreadsheet. You can obtain this address from Share > Publish as webpage > CSV dropdown. Also, be aware that when you make changes to the spreadsheet, it could take a few minutes for the changes to take effect. You can speed this up by clicking the Republish now button, found in the same overlay window.


Final Words

You can use the same technique to power different kinds of dynamic pages. However, this implementation does have its shortcomings. All the content is generated with JavaScript, which means that it will not be visible to search engines.

To guarantee that your data will be crawled, you can take a different route. You could use PHP, or other back-end language, to fetch and display the data from YQL in a fixed interval of time - say 30 minutes (or even less frequently, if you don't plan to update the data often).

Be sure to share your suggestions in the comment section below.

Bootstrap Studio

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

Learn more

Related Articles


excellent... was just thinking this morning how I could get a non-savvy client (who doesnt want a CMS) to update his football fixture list.
Google docs it is.

Great timing - great tutorial, ill put it into practice asap.

Thanks for tweeting! ;)

Wayne D.

Holy cow! This is awesome. It's exactly what we need for our own FAQ section. I'm also wondering how I can incorporate Gdocs into other pages to make then dynamic. Thank you very much for this crazy good Tut. Keep it up.

Fernando Lee

Great work on this tutorial, was really helpfull


very very nice working thanks for sharing

Keep up the good work. It is fantastic inspiring.

This is by far one of the best tuts ever. It takes 'open source' to a whole new level. Thanks Martin.

This tutorial came in the best timing for us! thanks

Wayne D.

When I generate a public CSV URL I get the following:

Which is a different URL structure(less characters) than your CSV URL.

I'm wondering if that is throwing off the JSON parser, because nothing is processed in my #faqSection div.

Nice idea and thanks for sharing!


This is really great tutorial. Thanks

Martin Angelov

@ Wayne D.

The CSV URLs do seem inconsistent, but I don't see any problems with it. Can you share a URL of your FAQ section? It might be some edge case I've failed to notice.


Awesome tut! YQL is the future.
Also checkout a YQL application which displays 2010 soccer wc statistics hosted on google docs: http://www.gdpint.com/QaweMlilo/fifa_2010

@Wayne: try changing your sharing settings on the csv document before generating your link.

This is nice. Easy to use and integrate CMS. Works fine. I'll make visual changes and use it on my future site. :)
Good luck and keep going straight!

oO WOW. Great idea. Thanks for sharing.

Nice script!

There seems to me something funky about the collapse/uncollapse in IE8. Any way to fix it?

David Calhoun

Very cool idea. Nice tutorial, Martin.

Wayne D.


Here is a URL for my FAQ:

I have made sure the sharing options to 'Public on the web.'

Would it matter if I was generating the CSV URL from a corporate Gmail account vs. a normal everyday Gmail account?

Thanks in advance.

Guys this is JUST amazing! :)

You totally ROCK! Thanks for this one ! :)


Perhaps it does matter. I first tested everything with a Google Apps Standard Edition account... none of the questions/answers were displaying.

I then repeated the steps with a normal everyday Google Docs account (that I was able to make the document be viewable to the entire public) and it worked just fine.

Martin Angelov

@ Wayne

As far as I can tell from your spreadsheet, its been malformed in some way. How did you create it? Maybe if you've copy/pasted it, the data has been corrupted.

Try to open mine, download it as an excel spreadsheet, and import it in your Google Docs account.

If each and every FAQ is manually expanded, you should change the "expand all" to "collapse all" button. Doncha think?


Why, so a user can tidy up the page before leaving? ;)


great! I wonder if i can use this to create a table with soccer scores.

Great tutorial this is awesome.


Its good practice and design as the user will be involved with the website.


Simple and neat!!! :) Minor problems in IE6 but that can be ignored...


I have to try and implement this myself but it looks pretty promising.
Really nice work!!

Really nice!


Never hear of this! Fantastic! I learn two things:
-Google API
Thank you so much for great article!

Once again a great tutorial Martin, thanks!

One side-note, if you want to use this on a secure, HTTPS site, be sure to adapt 'script.js' so that:
var yqlURL = "http://...
changes to:
var yqlURL = "https://...
This way, you won't get any annoying messages from your browser saying that unsecured content has been included in the page.

You could also abandon YQL altogether and only use PHP. (I don't see the point of using both of them, as Martin suggests at the end of the article.) To do so you could use the code below. Just insert it into 'faq.html' file where it says:""
$sCSVfile = 'https://spreadsheets.google.com/pub?key=0Ahe1-YRnPKQ_dEI0STVPX05NVTJuNENhVlhKZklNUlE&hl=en&output=csv';;

if (($handle = fopen($sCSVfile, "r")) !== FALSE)
while (($aCSVdata = fgetcsv($handle, 1000, ",")) !== FALSE)
echo '' . $aCSVdata[0] . '' . PHP_EOL;
echo '' . $aCSVdata[1] . '' . PHP_EOL;
That's it! You can now clean up 'script.js' and delete all the stuff related to YQL. (I hope the code above will show up decently after posting this comment.)

Martin Angelov

Very useful, Bas!

You are right, if you are to use PHP, you do not need YQL - PHP's got CSV support built in.

Hi Martin,

First of all, thanks for all your wonderful tutorials. Keep up the great work.
Two things:

1) Tutorial suggestion: rss reader with lots of jQuery bells and whistles

2) Question from a beginner (so sorry in advance if it is trivial) : I notice that in a lot of your tutorials, you use images for backgrounds, even if the background is just one color. Is there any advantage of this method vs using background-color ?


Martin Angelov

Thank you for your suggestion! You might want to check out this tutorial from earlier this year.

As for the background, the background images I use actually have a smooth gradient, they are not solid colors. If you want just a plain color for background, you should specify it in the background-color property instead of including it as a separate image.

Isn't there a rate limit for accessing Google Doc Spreadsheets?

I know there is for YQL but not sure about Google Docs.

Martin Angelov

YQL caches the results it gets from Google's Spreadsheet CSV resource, so I doubt you'd ever hit a rate limit.

Also, the limits for YQL are IP based, which means that every visitor is assigned an individual limit.


Wow, i've the good meal. thank for the guide.

Scott Hernandez

Just to clarify for those that want to use PHP (which is what I ended up doing, since we want it searchable), be sure to place the code inside the #faqSection div tags and also drop the '' tags before and after the PHP code. Also, where the quotes are in the code, be sure to drop in the '', '', and '' tags. It should look like this (I sure hope this comes out right in the comments - I don't want to confuse people):

$sCSVfile = 'https://spreadsheets.google.com/pub?key=0Ahe1-YRnPKQ_dEI0STVPX05NVTJuNENhVlhKZklNUlE&hl=en&output=csv';;
if (($handle = fopen($sCSVfile, "r")) !== FALSE)
while (($aCSVdata = fgetcsv($handle, 1000, ",")) !== FALSE)
echo '' . $aCSVdata[0] . '' . PHP_EOL;
echo '' . $aCSVdata[1] . '' . PHP_EOL;

Also, in the script, just delete the whole top section up to the '$('dt').live('click',function() {'

Hope that helps those that want to use the PHP version. Kudos, Martin - you're the best. And thanks, Bas, for providing the PHP to get it working. That was just what I needed.


Scott Hernandez

Hmm. It did strip the HTML, maybe that's why it was slightly confusing to use the PHP snippet by Bas. Oh well, we're all developers. Hopefully you guys get the point. Just trying to help. :)

Scott Hernandez

I don't know if this is possible or too complicated, but is there a way to start with the second row of a given column? (i.e. Skip the first row if you wanted to keep "Questions" and "Answers" so people editing the document knew what to do?) The answer could be using PHP (preferably for me) or JS. Either would be great to know. Thanks!

Thx for the tut!

If there is only one entry in the CSV the looping does not work !
Any ideas ?!?

Thank you

Thanks a lot! It is a great solution for managing a web page's content. I even created a menu and sub options using a second spreadsheet.

However, it is necessary to consider replacing all commas to ‚ or something like this in the document since the output for the spreadsheet is csv and values or texts containing commas could cause a column miss match problem. Just Ctrl + f and find: , and replate: ‚ it worked for me.


I'm afraid I don't know what you mean "find: , and replace: ‚ " ?? I see there is a slight difference in commas but what is it exactly?

Mostly because I keep having trouble getting my spreadsheet to work, I didn't know if it had to do with any security settings. Because here is my link:

..and here is one that DOES WORKS:

Notice the lack of an '&authkey' field. I've fiddled up and down with the document settings. What does that refer to, and how is it applied??


Works great on my site, EXCEPT the fact that when I change the position for the Expand/Collapse image because it was extending past my container...the hover image is totally lost. Yet, when its either Expand All, or Collapse All, the image works (?).

I've actually tried doing without the sprite and setting separate hover images, but it STILL doesn't work. What am I doing wrong?

My page is located in my name link above. Thanks for any of your help, and a great tutorial!


Any updates on the 'authkey' issue? How is this applied?

smack realized that the hover issue was caused by my theme and the content div's styling...background:none ;op

Though are there any more thought on the auth key in the Google Docs link?


Hello Martin,

Thanks for sharing this, its very helpful, but i have a question.

I like put the result (the google doc) a specific cell in to a specific place on my website. you have any advise about this.

In concret. i need 4 cells w/Diferent Prices and this put in diferentes pages (unique result)

In advance thanks a lot for take your time to view this.


I implemented this on a clients website a while ago - love it!! - but after the last edit to the google doc the FAQs are displaying twice on the page. There should only be 21 but they repeat again after the last one.

Any idea why that could be happening?

I'm using the PHP version fyi... here is the page... http://edit911.com/faq/

Thanks everyone!

Martin Angelov

You have implemented the PHP version, but the script still makes a call to YQL and includes the FAQs on the page. You need to disable the YQL call in the code (it is also going to make it shorter).


Thanks so much for the terrific tutorial. It's been working really well on our site. I have a problem, now, and I'm not sure why. And I'm not even sure when it started.

It works great in every browser except IE (I'm currently running IE 7), in which no FAQs populate at all.

The error I got was as follows:

Message: 'query.results' is null or not an object
Line: 17
Char: 3
Code: 0
URI: http://www.creighton.edu/fileadmin/user/Admissions/code/FAQ_transfer_script.js

Do you have any idea what could be going on?

Hi Christian

Did you manage to resolve the issue with the script not working on ie7. I have had the script on my site for sometime now and its been brilliant but since the beginning of this week it just stopped working on ie8 as well.

Please advice.


jimmy arias

Hi Robert !! do have the correct script for running in IE ?? please share that man,


I subscribe to previous speaker, there is options for solving this problems. The script works fine in all browsers except IE, given that a large percentage of people are still sitting on IE. The script need to be repaired.

What's needed to get this to work in IE9?

just replace jquery.js with version 1.5.1 or higher, the script "script.js"
will be able to work on all IE version.


Thanks for the reply.
Tried 1.7.1 from google but no cigar.


I've been using a modified version of this code and it has stopped working for me as well...however...some reason for me...I'm not sure as to the reason why, one out of the many pages I implement this on works on ie...they do not work on the rest. :/

It was probably just a coding goof, as I'm more of a designer than I am of a programer...but for some darn reason...it works..

I've tried to diagnosis and see why this one page, out of all the others actually functions (because it is built differently)...what has triggered it to function...but I really don't know.

Anyone who wants to take a look to see if they know why...it might provide a clue as to how it could function on the rest of the pages: http://www.academytheatre.org/

In the mean time, I will implement Anake's technique. -- I thought it might have to do with outdated jquery...but I wasn't sure. I'm going to test that out now. :)

not working in IE9. opera is fine!

tried replace jquery.js to the latest version 1.7.1, but not working...

Ditto on not working in IE9. YQL is returning this error:

jsonp1327428266882({"error":{"lang":"en-US","description":"Error Retrieving Data from External Service"}});

Any ideas?!?! It works great on every other browser.

Exactly what I was looking for!

Anyone have luck with the IE issues? I am also having the null result issue :/

Great Tutorial! After reading about the issues with IE9, I was wondering if there was a fix yet. Also, is there a way to make this work when js is disabled in the browser at all?

Hey Martin,
This is a very useful tutorial. Thanks for this.
I have one questions: Is this working in IE? i couldn't make it to work. do you have any suggestions why the json is giving an error on IE?

Is there already somebody who got this thing running on IE?

Works fine on IE for me.

David Ballard

I have the same issue with IE. Have built an extended faq using this excellent tutorial but now find it cannot be used, even manually, in IE. I would be very grateful if anyone could share a solution or a work around. Thanks

Gabor Ronai

Hi, it's a fantastic tutorial. Just one question: is there a way to place a clickable link in this FAQ. Thanks

Doesn't work on Explorer :(

Has anyone found a solution to this on IE? We have it working in Safari and Firefox, but most of our end users user IE and therefore it's a buzzkill.

Any help or suggestions would be appreciated! Thanks!

Julian Gon

It works on IE. I spent some time debugging this issue, and came to the conclusion that the problem is the "csvURL". It seems that by the time Martin Angelov put this beautiful tutorial together (August 11th, 2010), google used to implement "https://spreadsheets.google.com/...&quot; for the url, and as I am writing this today (May 8th, 2013), the url format is "https://docs.google.com/spreadsheet/...&quot;; so I believe that is why his example does not work in IE (but for some reason it does on Firefox. I wish I could explain the WHY but at this moment I don't have an answer for it). Also, I did some search thru google, and was able to find this as well [http://stackoverflow.com/questions/9421499/why-wont-my-simple-yql-json-via-jquery-script-parse-in-internet-explorer]. In conclusion, by using $.ajax instead of $.getJSON, and creating my own spreadsheet table which would give me the latest url structure, I was able to put this together [http://jsfiddle.net/gkyXe/]. I hope this is of any help.