I have received a few emails asking how I made the poll on my home page. The poll is embedded directly in the page (it doesn’t redirect you to Google Forms), and it updates in real time. It can also be styled any way you want.

1. Make a Google Form

  1. Go to Google Forms
  2. Make a blank form, add a multiple-choice question (i.e. “How do you manage your blog?”)
    • Optionally, set it as Required.
  3. Click Publish and set the form is set so Anyone with the link can respond
  4. Copy the responder link. The form’s FORM_ID will look like 1FAIpQLSfExampleFormIDHere12345. It is the long string between /e/ and /viewform

2. Get Pre-Filled Form

  1. In your form, click ⋮ (More) → Pre-fill form.
  2. Select any option and click Get link → COPY LINK.
    • The pre-filled URL will look something like: https://docs.google.com/forms/d/e/1FAIpQLSfExampleFormIDHere12345/viewform?usp=pp_url&entry.0123456789=This+is+an+option
  3. Copy the part that looks like entry.0123456789. This is your entry ID
  1. Back to your form (not pre-filled), click Responses → Link to Sheets. Link it to a new sheet.
  2. Copy the Sheet ID from the URL: https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit The SPREADSHEET_ID is the long string between /d/ and /edit
  3. Click Share and set the sheet is set so Anyone with the link can view

3. Basic HTML Form

<form id="pollForm">
  <label><input type="radio" name="entry.0123456789" value="Option A">Option A Label</label><br>
  <label><input type="radio" name="entry.0123456789" value="Option B">Option B Label</label><br>
  <label><input type="radio" name="entry.0123456789" value="__other_option__" id="otherRadio">Other:</label>
  <input type="text" id="otherInput" name="entry.0123456789.other_option_response"><br><br>
  <button id="voteBtn" type="submit" disabled>Vote</button>
  <button id="resultsBtn" type="button" disabled>View Results</button>
</form>

<section id="results"></section>

Replace entry.0123456789 with the actual entry ID you copied earlier. Make sure the Value matches exactly what you typed in your form (but the label text can be different if you wish).

  • If you include an “Other” option, use the format above with a text input for custom responses.
  • If not, you can just use regular radio buttons.

4. Javascript

document.addEventListener('DOMContentLoaded', function() {
  // === CONFIGURATION ===
  const FORM_ID = "YOUR_FORM_ID";       // Google Form ID (from the form's URL)
  const SHEET_ID = "YOUR_SHEET_ID";     // Google Sheet ID (linked to form responses)
  const STORAGE_KEY = `pollVoted_${FORM_ID}`; // LocalStorage key to track if this browser already voted

  // === DOM ELEMENTS ===
  const form = document.getElementById("pollForm");
  const voteBtn = document.getElementById("voteBtn");
  const resultsBtn = document.getElementById("resultsBtn");
  const resultsSection = document.getElementById("results");
  const otherRadio = document.getElementById("otherRadio");
  const otherInput = document.getElementById("otherInput");

  // === INITIAL STATE ===
  // If this browser already voted, disable the vote button but allow viewing results
  if (localStorage.getItem(STORAGE_KEY) === "true") {
    voteBtn.disabled = true;
    resultsBtn.disabled = false;
  }

  // === ENABLE VOTING WHEN USER SELECTS AN OPTION ===
  form.querySelectorAll("input[name='entry.0123456789']").forEach(radio => {
    radio.addEventListener("change", () => {
      // Enable "Other" text input only if "Other" is selected
      if (otherRadio.checked) {
        otherInput.disabled = false;
      } else {
        otherInput.disabled = true;
        otherInput.value = "";
      }

      // Enable vote button if user hasn’t already voted
      if (!localStorage.getItem(STORAGE_KEY)) {
        voteBtn.disabled = false;
      }
    });
  });

  // === HANDLE FORM SUBMISSION ===
  form.addEventListener("submit", function(e) {
    e.preventDefault(); // Prevent page reload

    // Block duplicate votes
    if (localStorage.getItem(STORAGE_KEY) === "true") {
      alert("You’ve already voted from this browser.");
      return;
    }

    // Require a selected option
    const selected = form.querySelector("input[name='entry.0123456789']:checked");
    if (!selected) {
      alert("Please select an answer.");
      return;
    }

    // Require text if "Other" is chosen
    if (otherRadio.checked && !otherInput.value.trim()) {
      alert("Please enter your custom answer.");
      return;
    }

    // Prepare data to send
    const formData = new FormData(form);

    // Submit vote directly to the Google Form
    fetch(`https://docs.google.com/forms/d/e/${FORM_ID}/formResponse`, {
      method: "POST",
      mode: "no-cors", // Required: avoids CORS issues but prevents reading response
      body: formData
    }).then(() => {
      // Mark as voted in localStorage
      localStorage.setItem(STORAGE_KEY, "true");

      // Update UI
      voteBtn.disabled = true;
      resultsBtn.disabled = false;
      resultsSection.innerHTML = "<p>Thanks for voting!</p>";
    }).catch(() => {
      alert("Error submitting vote. Please try again.");
    });
  });

  // === FETCH AND DISPLAY RESULTS ===
  resultsBtn.addEventListener("click", function() {
    resultsSection.textContent = "Loading results...";

    // Google Sheets "gviz" endpoint returns JSON wrapped in a function call
    fetch(`https://docs.google.com/spreadsheets/d/${SHEET_ID}/gviz/tq?`)
      .then(res => res.text())
      .then(text => {
        // Extract JSON from wrapper
        const match = text.match(/google\.visualization\.Query\.setResponse\((.*)\);/s);
        if (!match) throw new Error("Invalid response format");
        const data = JSON.parse(match[1]);

        // Each row is one form response
        const rows = data.table.rows;
        const counts = {};

        // Count responses by answer
        rows.forEach(row => {
          const answer = row.c[0]?.v; // Column 0 contains the answer
          if (answer) counts[answer] = (counts[answer] || 0) + 1;
        });

        // Render results
        let html = "<h3>Results</h3><ul>";
        for (const [answer, count] of Object.entries(counts)) {
          html += `<li>${answer}: ${count}</li>`;
        }
        html += "</ul>";
        resultsSection.innerHTML = html;
      })
      .catch(err => {
        resultsSection.textContent = "Error loading results: " + err.message;
      });
  });
});

Replace all instances of entry.0123456789 with the actual entry ID you copied earlier, as well as the FORM_ID and SHEET_ID.

How It Works

  1. Votes are submitted directly to Google Forms through its hidden form endpoint
  2. Results are retrieved from the linked Google Sheet via the gviz API
  3. Duplicate votes are blocked using localStorage (per browser/device)
    • Each new poll automatically uses a new storage key, since it’s based on the FORM_ID.

That’s it! This is the basic skeleton of the code I use for my polls. It doesn’t include any styling, so you’ll need to handle that part yourself. I hope this helps!