Build a Congressional Vote Tracker

Fetch live U.S. Congress data from an API and build an interactive vote tracker.

Introduction

30 Second Summary

Ever wondered how your elected representatives actually vote in Congress? Most people have no idea whether their senator or house member voted for or against the bills that affect their daily lives.

In this project, you will build a Congressional Vote Tracker website that pulls live data from a government-sourced API. You will display congress members by state, filter them by party, and drill into their real voting records.

What You'll Build

Pick any U.S. state from a dropdown, watch color-coded member cards fill the screen, then click one to reveal exactly how they voted on recent bills.

By the end of this project, you'll have:

  • A live website that fetches real congress member data from the CIV.IQ civic data API when you select any U.S. state, displaying representatives and senators as party-colored cards.
  • Interactive client-side filters that instantly narrow the member grid by political party, with the page updating in real time as you change your selection.
  • A drill-down voting history view where you click any member and see their recent votes color-coded green (Yea), red (Nay), or gray (Not Voting), with a filter to isolate specific positions.
  • Secret Mission: a live search bar that filters members by name as you type, combining with the party filter for multi-criteria results.

Are there any prerequisites?

None. This project starts from scratch and walks you through every tool you need. You will set up VS Code and a browser extension in Step 1 before writing any code.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters to you. Taking a moment to name your goal makes the rest of the build more intentional.

Set Up the Project Files

Your project folder has three empty files waiting to become a real web page. Before you can fetch data or build filters, you need the foundational structure that tells the browser what to display and how to style it.

In this step, you will write the full HTML skeleton that defines every section of the tracker. Then you will add CSS to give the page a clean, polished look.

In this step, get ready to:
  • Build the HTML structure with all sections (header, filters, member grid, voting history, footer).
  • Add CSS reset and base styling for the header and layout.
  • Launch Live Server and preview your page in the browser.
Add the page skeleton to your HTML

Every web page starts with an HTML document that defines the structure of what appears on screen. You will build this file in three parts, checking your progress along the way.

  • In VS Code, click on index.html in the Explorer sidebar to open it.
  • Add the document head and page header by pasting this code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Congressional Vote Tracker</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>Congressional Vote Tracker</h1>
        <p>See how your representatives vote in Congress</p>
    </header>

What does this code do?

The <head> section loads your stylesheet and sets the page title that appears in the browser tab.

The <header> element creates the visible banner at the top of your page with the project title and subtitle.

  • Save index.html by pressing Ctrl+S.
  • Right-click index.html in the Explorer sidebar and select Open with Live Server.

Your browser opens to http://localhost:5500. You should see the heading "Congressional Vote Tracker" and the subtitle on a plain white page.

Don't see the heading?

Make sure you saved the file (look for the dot on the tab title that disappears after saving). Also check that you right-clicked the file in the sidebar, not in the editor.

If "Open with Live Server" does not appear in the menu, the extension may not have finished installing. Press Ctrl+P, type ext install ritwickdey.liveserver, and press Enter to try again.

Still stuck? Help me troubleshoot why Live Server isn't opening my page.

  • Now add the main content area directly below the </header> line. This contains the filter dropdowns and the member card grid:

    <main>
        <section class="filters">
            <div class="filter-group">
                <label for="state-select">State</label>
                <select id="state-select">
                    <option value="">Select a state</option>
                </select>
            </div>
            <div class="filter-group">
                <label for="party-filter">Party</label>
                <select id="party-filter">
                    <option value="all">All Parties</option>
                    <option value="Democrat">Democrat</option>
                    <option value="Republican">Republican</option>
                    <option value="Independent">Independent</option>
                </select>
            </div>
        </section>

        <section class="members-section">
            <h2>Members of Congress</h2>
            <div id="members-container" class="members-grid">
                <p class="placeholder-text">Select a state to see its representatives.</p>
            </div>
        </section>

What does this code do?

The filters section holds two dropdowns. One lets users pick a U.S. state. The other filters members by political party.

The members-section is where JavaScript will later inject member cards. For now it shows placeholder text so you know the area exists.

  • Save index.html and check your browser.

You should see the two dropdown labels ("State" and "Party"), the dropdowns themselves, a "Members of Congress" heading, and the placeholder text "Select a state to see its representatives."

Page looks the same as before?

Check that the new code sits directly below </header> with no duplicate tags. If you accidentally pasted inside the header, the layout breaks. Undo with Ctrl+Z and try again.

Need help? My dropdowns and placeholder text are not appearing after I saved.

  • Complete the HTML file by adding the voting history section, footer, and closing tags directly below the </section> that closes members-section:

        <section id="votes-section" class="votes-section hidden">
            <div class="votes-header">
                <button id="back-button">Back to Members</button>
                <h2 id="votes-title">Voting History</h2>
                <div class="filter-group">
                    <label for="vote-filter">Filter Votes</label>
                    <select id="vote-filter">
                        <option value="all">All Votes</option>
                        <option value="Yea">Yea</option>
                        <option value="Nay">Nay</option>
                        <option value="Not Voting">Not Voting</option>
                    </select>
                </div>
            </div>
            <div id="votes-container" class="votes-list"></div>
        </section>
    </main>

    <footer>
        <p>Data provided by <a href="https://civdotiq.org" target="_blank">CIV.IQ</a> | Official government sources</p>
    </footer>

    <script src="script.js"></script>
</body>
</html>

What does this code do?

The votes-section starts with the class hidden. It will only appear when a user clicks on a member card. JavaScript handles showing and hiding it later.

The <script src="script.js"> tag at the bottom loads your JavaScript file after the HTML is fully parsed. This ensures all page elements exist before your code tries to find them.

  • Save index.html and check your browser.

You should see a new line at the bottom of the page: "Data provided by CIV.IQ | Official government sources". The votes section is intentionally invisible for now.

Footer text not showing?

Make sure the </main> closing tag appears before <footer>. If <footer> is nested inside <main> accidentally, it may still render, but check your indentation to catch misplaced tags early.

Need help? My footer is not appearing at the bottom of the page.

✔️ Awesome, I've got everything!

Great. Double check you saved index.html and your browser shows the heading, dropdowns, placeholder text, and footer.

ⓧ I'd like to double check the full code

Your index.html should contain all three chunks in order. Scroll up and confirm you have:

  • Chunk 1: The <!DOCTYPE html> declaration through the closing </header> tag.
  • Chunk 2: The <main> opening through the closing </section> of members-section.
  • Chunk 3: The votes-section opening through the closing </html> tag.
Style the page with CSS

The page structure is in place but everything looks plain. A CSS reset removes browser default spacing, and base styles give the header its dark background.

  • Click on style.css in the Explorer sidebar to open it.
  • Add the CSS reset and body styles by pasting this code:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: #f0f2f5;
    color: #1a1a2e;
    line-height: 1.6;
}

What does this code do?

The * selector removes all default margin and padding from every element. This gives you a clean slate so spacing is always intentional.

The body rule sets a system font stack, a light gray background, and comfortable line spacing for readability.

  • Save style.css and check your browser.

The page background changes from white to a light gray, and the text tightens up as default margins disappear.

Background still white?

Confirm the <link rel="stylesheet" href="style.css"> line exists in your index.html head. The filename must match exactly, including the lowercase .css extension.

Need help? My CSS styles are not applying to the page.

  • Below the body rule, add the header and main layout styles:
header {
    background-color: #1a1a2e;
    color: white;
    padding: 24px;
    text-align: center;
}

header h1 {
    font-size: 1.8rem;
    margin-bottom: 4px;
}

header p {
    font-size: 1rem;
    opacity: 0.8;
}

main {
    max-width: 1200px;
    margin: 0 auto;
    padding: 24px;
}

What does this code do?

The header gets a dark navy background with white text, creating a strong visual banner at the top.

The main element is capped at 1200px wide and centered with margin: 0 auto. This keeps content readable on large screens without stretching edge to edge.

  • Save style.css and check your browser.

Before you look, what do you expect the header to look like now?

The header should now display as a dark navy bar with white centered text. The main content below sits inside a comfortable padded container.

Header still has no background?

Make sure there is a blank line between the closing } of the body rule and the header { line. Missing a closing brace on the previous rule will cause all subsequent rules to fail silently.

Need help? My header background is not turning dark navy.

✔️ Awesome, I've got everything!

Great. Double check you saved style.css and your page shows the dark header with centered white text.

ⓧ I'd like to double check the full code

Your style.css should contain both chunks in order. Scroll up and confirm you have:

  • Chunk 1: The * reset selector and the body selector.
  • Chunk 2: The header, header h1, header p, and main selectors.

Your page skeleton and base styling are complete. Next up, you will write JavaScript that fetches real congress member data and displays it in the member grid.

Fetch and Display Members

Your development environment is ready and Live Server is running. Now it is time to prove the API works by pulling real congress member data onto the page.

The fastest way to bring your site to life is to connect it to a live data source. In this step, you will call the CIV.IQ civic data API, parse the response, and render member cards in a grid.

In this step, get ready to:
  • Write an async function that fetches congress member data from the CIV.IQ API.
  • Render each member as a card showing their name, party, title, and district.
  • Style the cards with CSS grid and color-coded party borders.
Set up the script data and populate the state dropdown

Every JavaScript app starts with data and DOM references. You need a list of all 50 states, a place to store fetched members, and pointers to the HTML elements your code will update.

  • Open script.js in VS Code and add the API base URL and state data:
// Base URL for the CIV.IQ civic data API
var API_BASE = "https://civdotiq.org/api/v1";

var states = [
    { code: "AL", name: "Alabama" }, { code: "AK", name: "Alaska" },
    { code: "AZ", name: "Arizona" }, { code: "AR", name: "Arkansas" },
    { code: "CA", name: "California" }, { code: "CO", name: "Colorado" },
    { code: "CT", name: "Connecticut" }, { code: "DE", name: "Delaware" },
    { code: "FL", name: "Florida" }, { code: "GA", name: "Georgia" },
    { code: "HI", name: "Hawaii" }, { code: "ID", name: "Idaho" },
    { code: "IL", name: "Illinois" }, { code: "IN", name: "Indiana" },
    { code: "IA", name: "Iowa" }, { code: "KS", name: "Kansas" },
    { code: "KY", name: "Kentucky" }, { code: "LA", name: "Louisiana" },
    { code: "ME", name: "Maine" }, { code: "MD", name: "Maryland" },
    { code: "MA", name: "Massachusetts" }, { code: "MI", name: "Michigan" },
    { code: "MN", name: "Minnesota" }, { code: "MS", name: "Mississippi" },
    { code: "MO", name: "Missouri" }, { code: "MT", name: "Montana" },
    { code: "NE", name: "Nebraska" }, { code: "NV", name: "Nevada" },
    { code: "NH", name: "New Hampshire" }, { code: "NJ", name: "New Jersey" },
    { code: "NM", name: "New Mexico" }, { code: "NY", name: "New York" },
    { code: "NC", name: "North Carolina" }, { code: "ND", name: "North Dakota" },
    { code: "OH", name: "Ohio" }, { code: "OK", name: "Oklahoma" },
    { code: "OR", name: "Oregon" }, { code: "PA", name: "Pennsylvania" },
    { code: "RI", name: "Rhode Island" }, { code: "SC", name: "South Carolina" },
    { code: "SD", name: "South Dakota" }, { code: "TN", name: "Tennessee" },
    { code: "TX", name: "Texas" }, { code: "UT", name: "Utah" },
    { code: "VT", name: "Vermont" }, { code: "VA", name: "Virginia" },
    { code: "WA", name: "Washington" }, { code: "WV", name: "West Virginia" },
    { code: "WI", name: "Wisconsin" }, { code: "WY", name: "Wyoming" }
];

// Store fetched data for filtering later
var allMembers = [];
var allVotes = [];

What does this code do?

API_BASE stores the root URL for every API request you will make. The states array holds all 50 U.S. state codes and names for the dropdown.

allMembers and allVotes are empty arrays that will hold fetched data so you can filter it without re-calling the API.

  • Below the code you just added, paste the DOM references, the initialization functions, and the init() call:
// Cache references to HTML elements
var stateSelect = document.getElementById("state-select");
var partyFilter = document.getElementById("party-filter");
var membersContainer = document.getElementById("members-container");
var votesSection = document.getElementById("votes-section");
var votesTitle = document.getElementById("votes-title");
var votesContainer = document.getElementById("votes-container");
var voteFilter = document.getElementById("vote-filter");
var backButton = document.getElementById("back-button");

// Wire up event listeners when the page loads
function init() {
    populateStateDropdown();
    stateSelect.addEventListener("change", handleStateChange);
    partyFilter.addEventListener("change", filterMembers);
    voteFilter.addEventListener("change", filterVotes);
    backButton.addEventListener("click", showMembers);
}

// Add each state as an option in the dropdown
function populateStateDropdown() {
    for (var i = 0; i < states.length; i++) {
        var option = document.createElement("option");
        option.value = states[i].code;
        option.textContent = states[i].name;
        stateSelect.appendChild(option);
    }
}

init();

What does this code do?

The DOM variables give your functions fast access to elements without searching the page each time. init() kicks off the app by populating the dropdown and registering event listeners for future interactions.

populateStateDropdown() loops through the states array and creates an <option> element for each one, then appends it to the select element.

  • Save script.js and refresh your browser.
  • Click the State dropdown. You should see all 50 states listed (Alabama through Wyoming).

Dropdown still empty?

  • Make sure you saved script.js after pasting both code blocks.
  • Check that your <script src="script.js"></script> tag is at the bottom of index.html, just before </body>.
  • Open DevTools (F12) and check the Console tab for typos. You may see an error about handleStateChange not being defined. That is expected and will resolve once you add the remaining functions below.

help me fix my state dropdown not populating

Fetch member data from the API

With the dropdown working, you need a function that fires when the user selects a state. This function will call the CIV.IQ API and store the response.

  • In script.js, add the handleStateChange function directly above the init(); line at the bottom:
// Fetch members when the user picks a state
async function handleStateChange() {
    var stateCode = stateSelect.value;
    if (!stateCode) {
        membersContainer.innerHTML = '<p class="placeholder-text">Select a state to see its representatives.</p>';
        return;
    }

    membersContainer.innerHTML = '<p class="loading">Loading members...</p>';
    partyFilter.value = "all";

    try {
        var response = await fetch(API_BASE + "/representatives?state=" + stateCode);
        var result = await response.json();
        allMembers = result.data;
        displayMembers(allMembers);
    } catch (error) {
        membersContainer.innerHTML = '<p class="placeholder-text">Error loading members. Please try again.</p>';
        console.error("Error fetching members:", error);
    }
}

What does this code do?

async lets you use await inside the function, pausing execution until the network request completes. The API returns a JSON object with a data array of member objects.

The try/catch block handles network failures gracefully by showing an error message instead of crashing. The loading text gives the user feedback while the request is in flight.

Why CIV.IQ instead of Congress.gov directly?

CIV.IQ requires no API key and no account. It normalizes data from Congress.gov, Senate.gov, and House Clerk sources into a single clean format. This removes a setup barrier so you can focus on learning JavaScript, not managing credentials.

  • Still above init();, add the helper functions that support filtering and navigation:
// Filter displayed members by the selected party
function filterMembers() {
    var selectedParty = partyFilter.value;
    if (selectedParty === "all") {
        displayMembers(allMembers);
    } else {
        var filtered = allMembers.filter(function (member) {
            return member.party === selectedParty;
        });
        displayMembers(filtered);
    }
}

// Create a click handler that captures the member ID
function createVoteLoader(bioguideId, memberName) {
    return function () {
        loadVotingHistory(bioguideId, memberName);
    };
}

// Switch back from votes view to members view
function showMembers() {
    votesSection.classList.add("hidden");
    document.querySelector(".members-section").classList.remove("hidden");
    document.querySelector(".filters").classList.remove("hidden");
}

What do these functions do?

filterMembers() checks the party dropdown value and uses JavaScript's .filter() method to show only matching members. You will wire this up fully in the next step.

createVoteLoader() returns a new function that remembers a specific member's ID. This pattern (a closure) is needed because adding event listeners inside a loop would otherwise lose track of which member was clicked.

  • Still above init();, add the displayMembers function that renders cards to the page:
// Render an array of members as cards
function displayMembers(members) {
    membersContainer.innerHTML = "";

    if (members.length === 0) {
        membersContainer.innerHTML = '<p class="placeholder-text">No members found matching your filters.</p>';
        return;
    }

    for (var i = 0; i < members.length; i++) {
        var member = members[i];
        var card = document.createElement("div");
        var partyClass = member.party.toLowerCase();
        card.className = "member-card " + partyClass;

        var district = member.district ? "District " + member.district : "Statewide";

        card.innerHTML =
            '<h3>' + member.name + '</h3>' +
            '<div class="member-details">' +
                '<span><span class="party-badge ' + partyClass + '">' + member.party + '</span></span>' +
                '<span>' + member.title + ' - ' + district + '</span>' +
                '<span>' + member.state + '</span>' +
            '</div>';

        card.addEventListener("click", createVoteLoader(member.bioguideId, member.name));
        membersContainer.appendChild(card);
    }
}

What does displayMembers do?

It clears the container, then loops through each member object and builds a card div with the member's name, a party badge, their title, district, and state.

Each card gets a CSS class matching the party name (democrat, republican, or independent) so you can color-code them with CSS. A click listener is attached so you can load voting history later.

Before you test, what do you think will happen when you select California from the dropdown?

  • Save script.js and refresh your browser.
  • Select California from the State dropdown.

You should see member cards appear in the container, each showing a name, party, title, and state. They will look unstyled for now. That is the next substep.

No cards appearing after selecting a state?

  • Open DevTools (F12) and check the Console for errors. A typo in a function name like displayMembers will prevent rendering.
  • Make sure all your functions are placed above the init(); call at the bottom of the file.
  • Confirm you see "Loading members..." text briefly. If it stays, the API call may be failing. Check the Network tab for a failed request.

help me debug why my member cards are not rendering

Style the member grid and cards

The cards are rendering, but they need layout and color to be useful. You will add CSS grid for the layout and colored left borders to distinguish parties at a glance.

  • Open style.css and add these filter and layout styles below your existing CSS:
.filters {
    display: flex;
    gap: 16px;
    margin-bottom: 24px;
    flex-wrap: wrap;
}

.filter-group {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.filter-group label {
    font-size: 0.85rem;
    font-weight: 600;
    color: #555;
}

.filter-group select {
    padding: 8px 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 1rem;
    background: white;
    cursor: pointer;
}

What does this CSS do?

The .filters section uses flexbox to lay the dropdowns out horizontally. Each .filter-group stacks its label above the select element with a small gap.

  • Below the filter styles, add the grid layout and base card styles:
.members-section h2 {
    margin-bottom: 16px;
    font-size: 1.3rem;
}

.members-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 16px;
}

.member-card {
    background: white;
    border-radius: 10px;
    padding: 20px;
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
    border-left: 4px solid #ccc;
}

.member-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.member-card.democrat {
    border-left-color: #2563eb;
}

.member-card.republican {
    border-left-color: #dc2626;
}

.member-card.independent {
    border-left-color: #7c3aed;
}

What does this CSS do?

CSS Grid with auto-fill creates a responsive multi-column layout that adjusts to the screen width. Each card gets a colored left border: blue for Democrat, red for Republican, purple for Independent.

  • Save style.css and refresh your browser.
  • Select California again. You should see the cards arranged in a grid with colored left borders.
  • Continue in style.css and add the card text and party badge styles:
.member-card h3 {
    font-size: 1.1rem;
    margin-bottom: 8px;
}

.member-card .member-details {
    font-size: 0.9rem;
    color: #555;
}

.member-card .member-details span {
    display: block;
    margin-bottom: 4px;
}

.party-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 0.8rem;
    font-weight: 600;
    color: white;
}

.party-badge.democrat {
    background-color: #2563eb;
}

.party-badge.republican {
    background-color: #dc2626;
}

.party-badge.independent {
    background-color: #7c3aed;
}

.hidden {
    display: none;
}

What does this CSS do?

The .party-badge styles create small colored pills that display the party name. The .hidden class toggles sections on and off when switching between views.

  • Finally, add the placeholder, loading, and footer styles at the bottom of style.css:
.placeholder-text {
    grid-column: 1 / -1;
    text-align: center;
    color: #777;
    padding: 40px;
    font-size: 1.1rem;
}

.loading {
    grid-column: 1 / -1;
    text-align: center;
    color: #555;
    padding: 40px;
    font-size: 1.1rem;
}

footer {
    text-align: center;
    padding: 24px;
    color: #777;
    font-size: 0.85rem;
}

footer a {
    color: #2563eb;
    text-decoration: none;
}

What does this CSS do?

grid-column: 1 / -1 makes the placeholder and loading text span the full width of the grid. The footer styles give the attribution link a clean appearance at the bottom of the page.

  • Save style.css and refresh your browser.
  • Select California from the dropdown. You should see polished member cards in a responsive grid, each showing the member's name, a colored party badge, their title and district, and the state.

Cards showing but not styled correctly?

  • Make sure your CSS class names match exactly: .member-card, .party-badge, .democrat, .republican, .independent.
  • Check that style.css is linked in your <head> tag with the correct filename.
  • Inspect a card in DevTools to confirm the democrat or republican class is applied to the div.

help me debug why my member card styles are not applying

✔️ Awesome, I've got everything!

Great. Double check you have saved both script.js and style.css.

ⓧ I'd like to double check the full code

Your script.js should contain these sections in order (scroll up to confirm each is present):

  • API_BASE variable and states array (all 50 states).
  • allMembers and allVotes empty arrays.
  • Eight DOM reference variables (stateSelect through backButton).
  • init() function with five lines inside.
  • populateStateDropdown() function.
  • handleStateChange() async function.
  • filterMembers() function.
  • createVoteLoader() function.
  • displayMembers() function.
  • showMembers() function.
  • init(); call at the very bottom.

Your style.css should contain these sections in order:

  • Reset (*), body, header, and main styles (from Step 1).
  • .filters, .filter-group, label, and select styles.
  • .members-section h2 and .members-grid (CSS grid).
  • .member-card base, hover, and party color variants.
  • Card text styles (h3, .member-details).
  • .party-badge and color variants.
  • .hidden, .placeholder-text, .loading, and footer styles.

Your site now pulls live government data and renders it as an interactive grid. Next up, you will add a state dropdown that re-fetches data and a party filter that narrows results in real time.

Add State Selection and Party Filter

In the previous step you proved the API works by fetching and displaying member data. But that data was for whichever state the dropdown happened to point at.

A hardcoded state is useless to anyone not from California. In this step, you will test the state dropdown that re-fetches live data and the party filter that narrows results client-side.

In this step, get ready to:
  • Select a different state and see new member cards load from the API.
  • Filter members by political party and confirm the grid updates.
  • Verify the loading state and party filter reset work together.
Understand how the state dropdown works

Your page already has a state dropdown in the HTML. The JavaScript populates it with all 50 states and re-fetches data whenever you change the selection.

  • In script.js, find the populateStateDropdown function:
function populateStateDropdown() {
    for (var i = 0; i < states.length; i++) {
        var option = document.createElement("option");
        option.value = states[i].code;
        option.textContent = states[i].name;
        stateSelect.appendChild(option);
    }
}

What does this function do?

This loops through the states array (all 50 U.S. states) and creates an <option> element for each one. Each option's value is the two-letter state code (like TX) and the visible text is the full state name.

  • Now find the handleStateChange function that runs whenever you pick a new state:
async function handleStateChange() {
    var stateCode = stateSelect.value;
    if (!stateCode) {
        membersContainer.innerHTML = '<p class="placeholder-text">Select a state to see its representatives.</p>';
        return;
    }

    membersContainer.innerHTML = '<p class="loading">Loading members...</p>';
    partyFilter.value = "all";

    try {
        var response = await fetch(API_BASE + "/representatives?state=" + stateCode);
        var result = await response.json();
        allMembers = result.data;
        displayMembers(allMembers);
    } catch (error) {
        membersContainer.innerHTML = '<p class="placeholder-text">Error loading members. Please try again.</p>';
        console.error("Error fetching members:", error);
    }
}

What does this function do?

  • It reads the selected state code from the dropdown.
  • It shows "Loading members..." text while the API call is in flight.
  • It resets the party filter back to "All Parties" so stale filters from the previous state don't carry over.
  • It calls the CIV.IQ API with the new state code and passes the results to displayMembers.
  • Switch to your browser where Live Server is running.
  • Click the State dropdown and select Texas.

You should briefly see "Loading members..." text, then a grid of Texas congress members appears with colored left borders.

Dropdown is empty or nothing happens?

  • Make sure script.js is saved and that the <script src="script.js"></script> tag is at the bottom of your HTML body.
  • Open DevTools with F12 and check the Console tab for red error messages.
  • Confirm the init() call exists at the very bottom of script.js.

Why isn't my state dropdown loading members when I change the selection?

Test the party filter

Now that Texas members are showing, the party filter lets you narrow the results without making another API call. The filtering happens entirely in JavaScript using the stored allMembers array.

  • In script.js, find the filterMembers function:
function filterMembers() {
    var selectedParty = partyFilter.value;
    if (selectedParty === "all") {
        displayMembers(allMembers);
    } else {
        var filtered = allMembers.filter(function (member) {
            return member.party === selectedParty;
        });
        displayMembers(filtered);
    }
}

What does this function do?

When you select a party, this function uses JavaScript's .filter() method to keep only members whose party field matches the dropdown value. It passes the filtered array to displayMembers which rebuilds the card grid.

No extra API call is needed because allMembers already holds every member for the current state. The CIV.IQ API uses full party names like Democrat and Republican, which match the dropdown option values exactly.

  • In your browser, make sure Texas is still selected in the state dropdown.
  • Click the Party dropdown and select Republican.

The grid should update instantly. Only cards with a red left border (Republican members) remain visible. The Democrat and Independent cards disappear.

  • Select All Parties from the party dropdown to bring all members back.

Party filter not changing anything?

  • Check that the option values in your HTML (Democrat, Republican, Independent) match the API's party field values exactly, including capitalization.
  • Confirm the event listener partyFilter.addEventListener("change", filterMembers) exists inside your init function.

Why is my party filter not updating the member cards?

Verify loading state and reset behavior

Two small details make the experience feel polished. The loading text gives feedback while the API responds. The party filter auto-reset prevents confusing stale results.

  • Select Democrat from the party filter so only Democrats show.
  • Now change the state dropdown to New York.

Before you look at the result, what do you expect the party filter dropdown to show after the state changes?

The party filter resets to All Parties automatically. New York's full list of members loads. This happens because handleStateChange sets partyFilter.value = "all" before fetching new data.

Why reset the party filter on state change?

Without this reset, switching from Texas (filtered to Republican) to a state with no Republicans would show an empty grid. The learner would think the API failed when the filter was just carrying over from the previous selection.

End-of-step check

Before running through the final verification, predict: if you select Texas and then filter to Republican, will you see more or fewer cards than the full Texas list?

  • Select Texas from the state dropdown.
  • Wait for the member cards to load.
  • Select Republican from the party filter.

You should see Texas members load in a grid, then the list narrows to only Republican members (red left border). Fewer cards remain visible than before the filter.

✔️ Awesome, I've got everything!

Great. Double check that you have saved script.js and index.html.

ⓧ I'd like to double check the full code

Your script.js should contain these key functions. Scroll through your file and confirm each one is present in this order:

  • API_BASE variable and states array (all 50 states).
  • allMembers and allVotes variables, then DOM references (stateSelect, partyFilter, membersContainer, etc.).
  • init() function with event listeners for stateSelect, partyFilter, voteFilter, and backButton.
  • populateStateDropdown() that loops through the states array.
  • handleStateChange() with loading text, partyFilter.value = "all", and the fetch call.
  • filterMembers() using .filter() on allMembers.
  • displayMembers(), createVoteLoader(), showMembers(), and the init() call at the bottom.

Your state selection and party filter are working together. Next up, you will add the ability to click any member card and see their actual voting history from Congress.

Show Voting History

Your tracker shows WHO represents a state. The real value is seeing HOW those representatives actually vote on legislation.

In this step, you'll make each member card clickable. Clicking a card fetches that member's recent voting record from the CIV.IQ API and displays each vote as a card showing the bill, date, and result.

In this step, get ready to:
  • Write a function that fetches a member's voting history from the API.
  • Display each vote as a card showing the bill title, date, and result.
  • Add a filter to show only Yea, Nay, or Not Voting positions.
Fetch the voting history when a member is clicked

Your createVoteLoader function already calls loadVotingHistory when a card is clicked. Now you need to write that function.

  • In script.js, find the createVoteLoader function near the bottom of your file.
  • Add the following loadVotingHistory function directly below it:
async function loadVotingHistory(bioguideId, memberName) {
    // Hide the members view and show the votes view
    document.querySelector(".members-section").classList.add("hidden");
    document.querySelector(".filters").classList.add("hidden");
    votesSection.classList.remove("hidden");
    votesTitle.textContent = memberName + " - Voting History";
    votesContainer.innerHTML = '<p class="loading">Loading votes...</p>';
    voteFilter.value = "all";

    try {
        var response = await fetch(API_BASE + "/representatives/" + bioguideId + "/votes");
        var result = await response.json();
        allVotes = result.votes || result.data || [];
        displayVotes(allVotes);
    } catch (error) {
        votesContainer.innerHTML = '<p class="placeholder-text">Error loading voting history. Please try again.</p>';
        console.error("Error fetching votes:", error);
    }
}

What does this code do?

This function handles the full flow of switching from the members view to the votes view.

  • It hides the members grid and filters by adding the hidden class, then reveals the votes section by removing it.
  • It sets the heading to the clicked member's name so you know whose votes you're viewing.
  • The API call uses the member's bioguideId (a unique government identifier) to fetch their specific voting record.
  • The defensive fallback result.votes || result.data || [] handles different response formats the API may return.
Display vote cards

The loadVotingHistory function calls displayVotes to render the fetched data. This function loops through the votes array and creates a card for each one.

  • Add the displayVotes function directly above showMembers:
function displayVotes(votes) {
    votesContainer.innerHTML = "";

    if (votes.length === 0) {
        votesContainer.innerHTML = '<p class="placeholder-text">No votes found matching your filter.</p>';
        return;
    }

    for (var i = 0; i < votes.length; i++) {
        var vote = votes[i];
        var card = document.createElement("div");
        card.className = "vote-card";

        // Pull bill info with fallbacks for missing data
        var billTitle = vote.bill ? vote.bill.title : (vote.description || "Vote");
        var billNumber = vote.bill ? vote.bill.number : "";
        var voteDate = vote.date ? new Date(vote.date).toLocaleDateString() : "Unknown date";

        card.innerHTML =
            '<h4>' + billTitle + '</h4>' +
            '<div class="vote-details">' +
                '<span>Position: ' + vote.position + '</span>' +
                '<span>Bill: ' + billNumber + '</span>' +
                '<span>Date: ' + voteDate + '</span>' +
                '<span>Result: ' + (vote.result || "N/A") + '</span>' +
            '</div>';

        votesContainer.appendChild(card);
    }
}

What does this code do?

  • It clears the container first, then handles the empty-results case with a placeholder message.
  • Each vote card shows the bill title, the member's position (Yea, Nay, or Not Voting), the bill number, the date, and the overall result.
  • The ternary fallbacks (like vote.bill ? vote.bill.title : (vote.description || "Vote")) protect against missing fields in the API response.
  • Save script.js.

Before you click a member card, what do you think will happen to the current view?

  • Return to your browser and select a state (if one is not already selected).
  • Click any member card.

The members grid disappears and you see a new view with the member's name in the heading, a list of their recent votes showing bill titles, positions, dates, and results.

  • Click the Back to Members button to return to the members grid.

The members list and filters reappear. You can click any other member to see their votes too.

Not seeing votes when you click a card?

  • Check the browser console (press F12) for errors. A typo in displayVotes or loadVotingHistory will show a ReferenceError.
  • Confirm loadVotingHistory is placed after createVoteLoader and displayVotes is placed above showMembers.
  • Make sure you saved the file. Unsaved changes will not be picked up by Live Server.

Why aren't my vote cards showing up when I click a member?

Filter votes by position

The votes section already has a Filter Votes dropdown in the HTML. Your init function already wires it to a filterVotes function. Now you need to write that function.

  • In script.js, add the filterVotes function between loadVotingHistory and displayVotes:
function filterVotes() {
    var selectedPosition = voteFilter.value;
    if (selectedPosition === "all") {
        displayVotes(allVotes);
    } else {
        // Show only votes matching the selected position
        var filtered = allVotes.filter(function (vote) {
            return vote.position === selectedPosition;
        });
        displayVotes(filtered);
    }
}

What does this code do?

This follows the same pattern as your party filter. It reads the dropdown value, filters the stored allVotes array to keep only votes whose position matches, then re-renders.

  • Save script.js.
  • Return to your browser and click any member card to view their votes.
  • Change the Filter Votes dropdown to Yea.

Only votes where the member voted Yea remain visible. Switch to Nay and only Nay votes appear. Select All Votes to see the full list again.

Filter dropdown not working?

  • Make sure filterVotes is a standalone function declaration (not inside another function). JavaScript hoists function declarations, so init can reference it from anywhere in the file.
  • Verify the dropdown option values in your HTML match exactly: Yea, Nay, Not Voting. These are case-sensitive and must match what the API returns.

Why is my vote filter not narrowing the results?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Your script.js now has three new functions. Scroll through your file and confirm these are present in order:

  • loadVotingHistory (after createVoteLoader).
  • filterVotes (after loadVotingHistory).
  • displayVotes (after filterVotes, before showMembers).

Your index.html and style.css are unchanged from the previous step.

You can now click any representative and see exactly how they voted on recent legislation. Next up, you'll add color-coding to make voting patterns jump off the screen at a glance.

Filter and Color-Code Votes

You can now click any member and see their voting history. But a plain list of votes with no visual distinction makes it impossible to spot patterns at a glance.

Color-coding each vote by position (green for Yea, red for Nay, gray for Not Voting) transforms raw data into something scannable and useful. You will also put the existing vote filter to work so users can isolate exactly the votes they care about.

In this step, get ready to:
  • Add color-coded position badges to each vote card.
  • Style badges green, red, or gray based on the member's vote position.
  • Verify the vote filter narrows results and shows an empty state when no votes match.
Add vote position color styles

Each vote from the API includes a position value like "Yea", "Nay", or "Not Voting". You will turn that value into a CSS class name and style each class with a distinct background color.

  • In style.css, scroll down to the existing .vote-card rule (the one with transition: box-shadow 0.2s).
  • Add the following styles directly below that rule to create a hover effect and the colored position badges:
/* Lift the card slightly on hover */
.vote-card:hover {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

/* Base style for the position badge */
.vote-card .vote-position {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 4px;
    font-size: 0.85rem;
    font-weight: 600;
    color: white;
    margin-bottom: 8px;
}

/* Color each position type */
.vote-position.yea {
    background-color: #16a34a;
}

.vote-position.nay {
    background-color: #dc2626;
}

.vote-position.not-voting {
    background-color: #6b7280;
}

.vote-position.present {
    background-color: #d97706;
}

What does this CSS do?

  • .vote-card:hover adds a subtle shadow when the user moves their mouse over a vote card.
  • .vote-card .vote-position styles the badge as a small rounded pill with white text.
  • Each position class (.yea, .nay, .not-voting, .present) sets a background color so the learner can identify vote positions at a glance.
  • Save style.css.
Update displayVotes to render color-coded badges

The CSS classes are ready but nothing in the HTML uses them yet. You need to update the displayVotes function so each vote card renders a <span> with the matching position class.

  • In script.js, find the displayVotes function. Inside the for loop, locate this line:
        var billTitle = vote.bill ? vote.bill.title : (vote.description || "Vote");
  • Add this new line directly above it to convert the position string into a CSS class name:
        // Convert "Not Voting" to "not-voting" for the CSS class
        var positionClass = vote.position.toLowerCase().replace(" ", "-");
  • Now find the card.innerHTML assignment below. It currently looks like this:
        card.innerHTML =
            '<h4>' + billTitle + '</h4>' +
            '<div class="vote-details">' +
                '<span>Position: ' + vote.position + '</span>' +
                '<span>Bill: ' + billNumber + '</span>' +
                '<span>Date: ' + voteDate + '</span>' +
                '<span>Result: ' + (vote.result || "N/A") + '</span>' +
            '</div>';
  • Replace that entire card.innerHTML block with this updated version that adds a color-coded badge and removes the plain-text position line:
        card.innerHTML =
            '<span class="vote-position ' + positionClass + '">' + vote.position + '</span>' +
            '<h4>' + billTitle + '</h4>' +
            '<div class="vote-details">' +
                '<span>Bill: ' + billNumber + '</span>' +
                '<span>Date: ' + voteDate + '</span>' +
                '<span>Result: ' + (vote.result || "N/A") + '</span>' +
            '</div>';

What changed here?

The position is no longer just text inside the vote-details div. It is now a separate <span> at the top of the card with a dynamic class like vote-position yea or vote-position not-voting.

The .toLowerCase().replace(" ", "-") call turns "Not Voting" into "not-voting" so it matches the CSS class name exactly.

  • Save script.js.

Before you check the browser, what color do you expect a "Yea" vote badge to be? What about "Nay"?

  • Switch to your browser. Select a state, then click any member card to view their votes.

Each vote card now shows a colored badge at the top. Yea votes have a green badge, Nay votes have a red badge, Not Voting shows in gray, and Present shows in amber.

Badges showing but not colored?

  • Make sure you saved both style.css and script.js. Live Server only picks up saved changes.
  • Check that the class in your CSS (.vote-position.yea) exactly matches the class your JavaScript generates. Open DevTools (F12) and inspect a badge element to see its actual classes.
  • If badges say "undefined", confirm you added the positionClass variable line above the billTitle line, not below it.

Why aren't my vote position badges showing the correct colors?

Test the vote position filter

The vote filter dropdown and the filterVotes function already exist from the previous step. Now that badges are color-coded, filtering becomes a much more satisfying visual experience.

What do you think will happen when you select "Nay" from the Filter Votes dropdown?

  • While viewing a member's votes, select Nay from the Filter Votes dropdown.

Only votes with a red "Nay" badge remain visible. The rest disappear.

  • Now select Not Voting from the same dropdown.

If the member has no "Not Voting" entries, you will see the message "No votes found matching your filter." This empty state prevents a confusing blank screen.

  • Select All Votes to reset, then click Back to Members to confirm the navigation still works.

How does the filter work?

The filterVotes function uses JavaScript's .filter() method to keep only votes whose position matches the dropdown value. It then passes the filtered array to displayVotes, which rebuilds the cards from scratch. This is the same pattern the party filter uses for members.

Filter not changing anything?

  • The dropdown option values must exactly match the API's position strings. Check that your HTML options say Yea, Nay, and Not Voting (case-sensitive).
  • Open the browser console (F12) and look for JavaScript errors. A typo in the event listener setup will silently break filtering.

Why is my vote position filter not filtering the list?

✔️ Awesome, I've got everything!

Great. Double check you have saved both style.css and script.js.

ⓧ I'd like to double check the full code

Scroll up and confirm the following are in place, in order:

  • In style.css: the .vote-card:hover rule appears directly after the .vote-card rule.
  • In style.css: the .vote-card .vote-position base rule exists, followed by .vote-position.yea, .vote-position.nay, .vote-position.not-voting, and .vote-position.present.
  • In script.js: inside the displayVotes loop, the positionClass variable appears above billTitle.
  • In script.js: the card.innerHTML starts with a <span class="vote-position ..."> element and does NOT have a plain-text "Position:" span inside vote-details.

Your Congressional Vote Tracker is fully functional. Users can pick any state, browse members, filter by party, drill into voting records, and filter votes by position. The color-coded badges make scanning a voting history effortless. Next up, a Secret Mission to make the app even more powerful.

Secret mission

Add Member Name Search

Your tracker lets users filter by party, but what if they already know which member they want? Add a live search bar that filters members by name as you type, and make it work together with the party filter so both criteria apply at once.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses a free, open API with no account or API key. There are no ongoing costs.

Decide whether to keep your project files to continue experimenting, or delete them to free up space.

Resources you used:

  • The vote-tracker folder on your Desktop (contains index.html, style.css, and script.js).
  • The Live Server extension in VS Code.

Keep everything running

No action needed. Choose this if you want to keep experimenting with the Congressional Vote Tracker or add more features.

  • Your vote-tracker folder stays on your Desktop, ready to open any time.
  • The Live Server extension remains installed in VS Code for future projects.

Pause - I'll come back to this later

Since everything is local files and a free API, there is nothing to pause. Your project is already idle when you close VS Code.

  • Close the Live Server by clicking Port: 5500 in the VS Code status bar (bottom right).
  • Close VS Code. Your files remain on the Desktop whenever you want to resume.

Delete - I don't want to use this again

Remove all project files and the extension to start fresh.

  • Close the Live Server if it is still running by clicking Port: 5500 in the VS Code status bar.
  • Uninstall the Live Server extension by opening the Extensions panel in VS Code (Ctrl+Shift+X), finding Live Server, and clicking Uninstall.
  • Delete the project folder by running this command in your terminal:
Remove-Item -Recurse -Force ~/Desktop/vote-tracker

Confirm the folder is gone by checking your Desktop. You should no longer see the vote-tracker folder.

Nice Work!

Nice Work!

You just built a complete Congressional Vote Tracker from scratch using HTML, CSS, and JavaScript. Your site pulls live data from a real government-sourced API and lets anyone explore how their representatives vote in Congress.

You've learned how to:

  • Fetch live data from a REST API using async/await and render it as interactive cards on the page.
  • Manipulate the DOM dynamically by creating elements with JavaScript, attaching event listeners, and showing or hiding entire sections based on user interaction.
  • Implement client-side filtering using array .filter() to narrow results by party and vote position in real time.
  • Secret Mission: Combined a real-time text search with a dropdown filter to create multi-criteria filtering that applies both conditions simultaneously.

Ready to quiz yourself?