Document Hub Dashboard

Build a responsive dark-themed dashboard with real-time search filtering.

Introduction

30 Second Summary

Think about searching through a massive archive of documents. You type a name and every related file surfaces instantly.

In this project, you will build a declassified document archive dashboard that displays historical documents as cards and filters them in real time as you type.

What You'll Build

A dark-themed, responsive dashboard that uses CSS Grid for layout, CSS custom properties for theming, and vanilla JavaScript for real-time search filtering.

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

  • A responsive CSS Grid layout that adapts from a multi-column desktop grid to a single-column mobile view.
  • A dark theme design system built with CSS custom properties for consistent colors across the entire dashboard.
  • A real-time search filter that matches documents by person name, title, or keywords as you type.
  • Secret Mission: Add interactive category filter tags that combine with text search for multi-criteria filtering.

Are there any prerequisites?

None. This guide includes setup for all required tools. You just need a computer with an internet connection.

Set Up Your Project

Every web project starts with a clean folder structure. Your Document Hub dashboard needs three core files working together: one for structure (HTML), one for styling (CSS), and one for interactivity (JavaScript).

In this step, you'll create the project folder, set up all three files, and confirm they load correctly in your browser.

In this step, get ready to:
  • Create the document-hub project folder on your Desktop.
  • Write HTML boilerplate that links the CSS and JavaScript files.
  • Verify the page displays correctly in Chrome using Live Server.
Create the project folder and files

You need a dedicated folder to keep your project's files organized in one place. You'll create this on your Desktop so it's easy to find.

  • In VS Code, click Terminal in the top menu bar.
  • Click New Terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir document-hub
cd document-hub
  • Create the three project files by running:
touch index.html style.css script.js
  • Confirm the files exist by running:
ls

You should see index.html, style.css, and script.js in the output.

Command not found for touch?

On Windows, touch may not be available in PowerShell. You can create the files by running this instead:

New-Item index.html, style.css, script.js

Alternatively, you can create the files later through VS Code's file explorer by right-clicking in the sidebar and selecting New File.

Help me create files in my terminal on Windows.

Write the HTML boilerplate

Now you need to open the project folder in VS Code so you can edit the files. The HTML file will serve as the entry point that connects your CSS and JavaScript together.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the document-hub folder, then click Open.
  • In the Explorer sidebar on the left, click index.html to open it in the editor.
  • Paste the following HTML into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document Hub - Declassified Archive</title>
    <!-- Link to the external CSS stylesheet -->
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- Test heading to confirm the page loads -->
    <h1>Document Hub</h1>
    <!-- Link to the external JavaScript file -->
    <script src="script.js"></script>
</body>
</html>

What does this code do?

  • The <link rel="stylesheet" href="style.css"> tag connects your CSS file so the browser knows where to find your styles.
  • The <script src="script.js"></script> tag connects your JavaScript file. It sits just before </body> so the HTML loads first before the script runs.
  • The <h1>Document Hub</h1> heading is a quick test to confirm everything is wired up correctly.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Launch Live Server and verify in Chrome

The Live Server extension from earlier creates a local web server that automatically refreshes your browser whenever you save a file. This means you'll see your changes instantly without manually reloading.

  • Make sure index.html is open in the editor.
  • Click the Go Live button in the bottom-right corner of the VS Code status bar.

What should I see?

Your default browser (Chrome) should automatically open a new tab showing a page with the heading "Document Hub" in large text on a white background. The URL bar will show something like 127.0.0.1:5500 or localhost:5500.

You should see "Document Hub" displayed as a large heading in your Chrome browser. This confirms your HTML file is loading correctly through Live Server.

Don't see the Go Live button?

The Go Live button only appears when you have a folder open in VS Code (not just a single file). Make sure you used File > Open Folder to open the document-hub folder.

If the browser doesn't open automatically, try opening Chrome manually and navigating to http://127.0.0.1:5500.

Help me get Live Server working in VS Code.

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html and your browser is showing the heading.

ⓧ I'd like to double check the full code

Here is the complete index.html file at this point. Your style.css and script.js files should still be empty.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document Hub - Declassified Archive</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Document Hub</h1>
    <script src="script.js"></script>
</body>
</html>

Your project structure is ready and confirmed working. Next up, you'll build out the full HTML structure for the dashboard with a header, search bar, and all 12 document cards.

Build the Dashboard HTML Structure

Your project files are connected and Live Server is running. Now it's time to build the actual structure of the dashboard.

HTML defines what content appears on the page and how it's organized. In this step, you'll replace the test heading with a full dashboard layout containing a header, a search bar, and a collection of document cards.

In this step, get ready to:
  • Create a header with the app title and a search input field.
  • Add a stats bar that displays the document count.
  • Build 12 document cards with titles, people, dates, types, and descriptions.
Create the header and search input

Every dashboard needs a header that tells users what they're looking at and a way to search the content. You'll wrap everything in a container div that centers the layout on the page.

  • In VS Code, open index.html.
  • Find the test heading line:
<h1>Document Hub</h1>
  • Select that entire line and replace it with the following code:
    <div class="dashboard">
        <header class="header">
            <div class="header-content">
                <h1 class="logo">Document Hub</h1>
                <p class="subtitle">Declassified Archive</p>
            </div>
            <div class="search-container">
                <input
                    type="search"
                    id="search-input"
                    class="search-input"
                    placeholder="Search by person, title, or keyword..."
                    autocomplete="off"
                >
            </div>
        </header>
    </div>

What does this code do?

The .dashboard div is the outer container that will center all content and set a maximum width for the page.

Inside the <header>, two sections sit side by side. The .header-content holds the app name and subtitle. The .search-container holds an input field where users will type search queries.

The type="search" attribute tells the browser this is a search field, which adds a small clear button in some browsers.

  • Save index.html.
  • Check your browser. You should see the text "Document Hub", "Declassified Archive", and a search input field. They won't be styled yet, so they'll appear as plain black text on a white background.

Don't see the search field?

Make sure the closing </div> tag for .dashboard appears after </header> and before the <script> tag. A missing closing tag can hide content.

Also confirm you deleted the old <h1>Document Hub</h1> line completely before pasting the new code.

Help me figure out why my header isn't showing up.

Add a stats bar showing the document count

A stats bar gives users a quick sense of how many documents are in the collection. Later, JavaScript will update this count as the user searches.

  • In index.html, find the closing </header> tag.
  • Add the following code on a new line below it, inside the .dashboard div:
        <div class="stats-bar">
            <span id="doc-count">Showing 12 of 12 documents</span>
        </div>

What does this code do?

The #doc-count span has an id so JavaScript can find it and update the text whenever the search results change. For now, it displays a static count.

  • Save index.html and check the browser.
  • You should see "Showing 12 of 12 documents" appear below the header text.
Build the document cards

Each document in the archive gets its own card. The cards use <article> elements, which is the correct HTML tag for self-contained pieces of content.

Every card stores two pieces of hidden data as attributes: data-person (the person's name in lowercase) and data-type (the document category). JavaScript will use these to power the search feature.

  • In index.html, find the closing </div> of the .stats-bar you just added.
  • Add the following code on a new line below it, still inside the .dashboard div:
        <main class="document-grid" id="document-grid">
            <article class="doc-card" data-person="john f. kennedy" data-type="report">
                <div class="doc-type-badge">Report</div>
                <h2 class="doc-title">Warren Commission Report Summary</h2>
                <p class="doc-person">John F. Kennedy</p>
                <p class="doc-date">September 1964</p>
                <p class="doc-description">Summary findings from the Presidential Commission on the assassination of President Kennedy in Dallas, Texas.</p>
            </article>

            <article class="doc-card" data-person="john f. kennedy" data-type="executive order">
                <div class="doc-type-badge">Executive Order</div>
                <h2 class="doc-title">Executive Order 11110</h2>
                <p class="doc-person">John F. Kennedy</p>
                <p class="doc-date">June 1963</p>
                <p class="doc-description">Delegation of authority to the Secretary of the Treasury regarding silver certificates and United States notes.</p>
            </article>

            <article class="doc-card" data-person="john f. kennedy" data-type="speech">
                <div class="doc-type-badge">Speech</div>
                <h2 class="doc-title">Inaugural Address</h2>
                <p class="doc-person">John F. Kennedy</p>
                <p class="doc-date">January 1961</p>
                <p class="doc-description">Ask not what your country can do for you. Historic inaugural address delivered at the United States Capitol.</p>
            </article>

            <article class="doc-card" data-person="abraham lincoln" data-type="speech">
                <div class="doc-type-badge">Speech</div>
                <h2 class="doc-title">Gettysburg Address</h2>
                <p class="doc-person">Abraham Lincoln</p>
                <p class="doc-date">November 1863</p>
                <p class="doc-description">A brief but powerful speech dedicating the Soldiers National Cemetery during the American Civil War.</p>
            </article>

            <article class="doc-card" data-person="abraham lincoln" data-type="executive order">
                <div class="doc-type-badge">Executive Order</div>
                <h2 class="doc-title">Emancipation Proclamation</h2>
                <p class="doc-person">Abraham Lincoln</p>
                <p class="doc-date">January 1863</p>
                <p class="doc-description">Presidential proclamation declaring the freedom of enslaved people in Confederate states during the Civil War.</p>
            </article>

            <article class="doc-card" data-person="abraham lincoln" data-type="memo">
                <div class="doc-type-badge">Memo</div>
                <h2 class="doc-title">Letter to Horace Greeley</h2>
                <p class="doc-person">Abraham Lincoln</p>
                <p class="doc-date">August 1862</p>
                <p class="doc-description">Response to the editor of the New York Tribune regarding the restoration of the Union and the question of slavery.</p>
            </article>

            <article class="doc-card" data-person="albert einstein" data-type="memo">
                <div class="doc-type-badge">Memo</div>
                <h2 class="doc-title">Einstein-Szilard Letter</h2>
                <p class="doc-person">Albert Einstein</p>
                <p class="doc-date">August 1939</p>
                <p class="doc-description">Letter to President Roosevelt warning of potential development of extremely powerful bombs using uranium chain reactions.</p>
            </article>

            <article class="doc-card" data-person="albert einstein" data-type="report">
                <div class="doc-type-badge">Report</div>
                <h2 class="doc-title">FBI Surveillance File Summary</h2>
                <p class="doc-person">Albert Einstein</p>
                <p class="doc-date">March 1955</p>
                <p class="doc-description">Declassified summary of the Federal Bureau of Investigation monitoring of Dr. Einstein's political associations and activities.</p>
            </article>

            <article class="doc-card" data-person="richard nixon" data-type="memo">
                <div class="doc-type-badge">Memo</div>
                <h2 class="doc-title">Watergate Tape Transcript Excerpt</h2>
                <p class="doc-person">Richard Nixon</p>
                <p class="doc-date">June 1972</p>
                <p class="doc-description">Transcribed excerpt from White House recordings related to the Watergate break-in and subsequent investigation.</p>
            </article>

            <article class="doc-card" data-person="richard nixon" data-type="speech">
                <div class="doc-type-badge">Speech</div>
                <h2 class="doc-title">Resignation Address</h2>
                <p class="doc-person">Richard Nixon</p>
                <p class="doc-date">August 1974</p>
                <p class="doc-description">Nationally televised address announcing the resignation of the presidency, the first in American history.</p>
            </article>

            <article class="doc-card" data-person="martin luther king jr." data-type="speech">
                <div class="doc-type-badge">Speech</div>
                <h2 class="doc-title">I Have a Dream</h2>
                <p class="doc-person">Martin Luther King Jr.</p>
                <p class="doc-date">August 1963</p>
                <p class="doc-description">Public speech delivered during the March on Washington calling for civil rights and an end to racism in the United States.</p>
            </article>

            <article class="doc-card" data-person="martin luther king jr." data-type="report">
                <div class="doc-type-badge">Report</div>
                <h2 class="doc-title">FBI COINTELPRO File Summary</h2>
                <p class="doc-person">Martin Luther King Jr.</p>
                <p class="doc-date">December 1968</p>
                <p class="doc-description">Declassified summary of the FBI Counter Intelligence Program surveillance operations targeting civil rights leaders.</p>
            </article>
        </main>

        <p class="no-results" id="no-results">No documents found. Try a different search term.</p>

What does this code do?

The <main> element with class .document-grid will become a CSS Grid container in the next step. For now it simply holds the 12 cards in document order.

Each card follows the same pattern: a .doc-type-badge showing the category (Memo, Speech, Report, or Executive Order), a title, the associated person's name, a date, and a short description.

The data-person attribute stores the person's name in lowercase so JavaScript can match searches without worrying about capitalization. The #no-results paragraph is hidden by default and will appear only when a search returns zero matches.

  • Save index.html.
  • Check your browser. You should see all 12 document titles listed on the page as plain, unstyled text. The document type, person name, date, and description for each card all appear as separate lines.

Only seeing some of the cards?

Make sure the closing </main> tag appears after the last </article> tag. If a card's closing </article> is missing, everything after it gets swallowed inside that card element.

Also check that the <p class="no-results"...> element sits outside the </main> tag but still inside the .dashboard div.

Help me debug my document cards HTML structure.

Why use data attributes?

HTML data-* attributes let you attach custom information to elements without affecting how they display. The browser ignores them visually, but JavaScript can read them instantly.

Storing the person's name in data-person means you can search by person without parsing through every child element. This keeps your search logic fast and simple.

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html and your browser shows all 12 documents as unstyled text.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document Hub - Declassified Archive</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="dashboard">
        <header class="header">
            <div class="header-content">
                <h1 class="logo">Document Hub</h1>
                <p class="subtitle">Declassified Archive</p>
            </div>
            <div class="search-container">
                <input
                    type="search"
                    id="search-input"
                    class="search-input"
                    placeholder="Search by person, title, or keyword..."
                    autocomplete="off"
                >
            </div>
        </header>

        <div class="stats-bar">
            <span id="doc-count">Showing 12 of 12 documents</span>
        </div>

        <main class="document-grid" id="document-grid">
            <article class="doc-card" data-person="john f. kennedy" data-type="report">
                <div class="doc-type-badge">Report</div>
                <h2 class="doc-title">Warren Commission Report Summary</h2>
                <p class="doc-person">John F. Kennedy</p>
                <p class="doc-date">September 1964</p>
                <p class="doc-description">Summary findings from the Presidential Commission on the assassination of President Kennedy in Dallas, Texas.</p>
            </article>

            <article class="doc-card" data-person="john f. kennedy" data-type="executive order">
                <div class="doc-type-badge">Executive Order</div>
                <h2 class="doc-title">Executive Order 11110</h2>
                <p class="doc-person">John F. Kennedy</p>
                <p class="doc-date">June 1963</p>
                <p class="doc-description">Delegation of authority to the Secretary of the Treasury regarding silver certificates and United States notes.</p>
            </article>

            <article class="doc-card" data-person="john f. kennedy" data-type="speech">
                <div class="doc-type-badge">Speech</div>
                <h2 class="doc-title">Inaugural Address</h2>
                <p class="doc-person">John F. Kennedy</p>
                <p class="doc-date">January 1961</p>
                <p class="doc-description">Ask not what your country can do for you. Historic inaugural address delivered at the United States Capitol.</p>
            </article>

            <article class="doc-card" data-person="abraham lincoln" data-type="speech">
                <div class="doc-type-badge">Speech</div>
                <h2 class="doc-title">Gettysburg Address</h2>
                <p class="doc-person">Abraham Lincoln</p>
                <p class="doc-date">November 1863</p>
                <p class="doc-description">A brief but powerful speech dedicating the Soldiers National Cemetery during the American Civil War.</p>
            </article>

            <article class="doc-card" data-person="abraham lincoln" data-type="executive order">
                <div class="doc-type-badge">Executive Order</div>
                <h2 class="doc-title">Emancipation Proclamation</h2>
                <p class="doc-person">Abraham Lincoln</p>
                <p class="doc-date">January 1863</p>
                <p class="doc-description">Presidential proclamation declaring the freedom of enslaved people in Confederate states during the Civil War.</p>
            </article>

            <article class="doc-card" data-person="abraham lincoln" data-type="memo">
                <div class="doc-type-badge">Memo</div>
                <h2 class="doc-title">Letter to Horace Greeley</h2>
                <p class="doc-person">Abraham Lincoln</p>
                <p class="doc-date">August 1862</p>
                <p class="doc-description">Response to the editor of the New York Tribune regarding the restoration of the Union and the question of slavery.</p>
            </article>

            <article class="doc-card" data-person="albert einstein" data-type="memo">
                <div class="doc-type-badge">Memo</div>
                <h2 class="doc-title">Einstein-Szilard Letter</h2>
                <p class="doc-person">Albert Einstein</p>
                <p class="doc-date">August 1939</p>
                <p class="doc-description">Letter to President Roosevelt warning of potential development of extremely powerful bombs using uranium chain reactions.</p>
            </article>

            <article class="doc-card" data-person="albert einstein" data-type="report">
                <div class="doc-type-badge">Report</div>
                <h2 class="doc-title">FBI Surveillance File Summary</h2>
                <p class="doc-person">Albert Einstein</p>
                <p class="doc-date">March 1955</p>
                <p class="doc-description">Declassified summary of the Federal Bureau of Investigation monitoring of Dr. Einstein's political associations and activities.</p>
            </article>

            <article class="doc-card" data-person="richard nixon" data-type="memo">
                <div class="doc-type-badge">Memo</div>
                <h2 class="doc-title">Watergate Tape Transcript Excerpt</h2>
                <p class="doc-person">Richard Nixon</p>
                <p class="doc-date">June 1972</p>
                <p class="doc-description">Transcribed excerpt from White House recordings related to the Watergate break-in and subsequent investigation.</p>
            </article>

            <article class="doc-card" data-person="richard nixon" data-type="speech">
                <div class="doc-type-badge">Speech</div>
                <h2 class="doc-title">Resignation Address</h2>
                <p class="doc-person">Richard Nixon</p>
                <p class="doc-date">August 1974</p>
                <p class="doc-description">Nationally televised address announcing the resignation of the presidency, the first in American history.</p>
            </article>

            <article class="doc-card" data-person="martin luther king jr." data-type="speech">
                <div class="doc-type-badge">Speech</div>
                <h2 class="doc-title">I Have a Dream</h2>
                <p class="doc-person">Martin Luther King Jr.</p>
                <p class="doc-date">August 1963</p>
                <p class="doc-description">Public speech delivered during the March on Washington calling for civil rights and an end to racism in the United States.</p>
            </article>

            <article class="doc-card" data-person="martin luther king jr." data-type="report">
                <div class="doc-type-badge">Report</div>
                <h2 class="doc-title">FBI COINTELPRO File Summary</h2>
                <p class="doc-person">Martin Luther King Jr.</p>
                <p class="doc-date">December 1968</p>
                <p class="doc-description">Declassified summary of the FBI Counter Intelligence Program surveillance operations targeting civil rights leaders.</p>
            </article>
        </main>

        <p class="no-results" id="no-results">No documents found. Try a different search term.</p>
    </div>

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

Your HTML skeleton is complete. The page looks plain right now, but all the content and structure is in place. Next up, you'll bring this to life with a dark theme and card layout using CSS.

Style the Dark Theme and Card Layout

Your HTML structure is complete, but right now the browser shows unstyled text on a white background. A dashboard needs a cohesive visual identity to feel professional and readable.

In this step, you'll transform that raw HTML into a polished dark-themed dashboard. You'll use CSS custom properties to define reusable color tokens and CSS Grid to arrange document cards in a responsive multi-column layout.

In this step, get ready to:
  • Define a dark color palette using CSS custom properties.
  • Style the header, search input, and stats bar.
  • Build a responsive card grid with styled document cards.
Define the color palette and base styles

CSS custom properties let you define color values once and reference them everywhere. Changing a single variable updates the entire theme instantly.

  • In VS Code, click the style.css tab to open the empty stylesheet.
  • Add the color palette, reset, and base layout by pasting this code:
/* Color palette - change any value here to retheme the entire dashboard */
:root {
    --bg-primary: #0f1117;
    --bg-secondary: #1a1d27;
    --bg-card: #232733;
    --text-primary: #e4e6eb;
    --text-secondary: #a0a3ab;
    --accent: #6c63ff;
    --accent-hover: #5a52d9;
    --border: #2e3241;
    --radius: 12px;
    --shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}

/* Reset default browser spacing */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* Base page styles */
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: var(--bg-primary);
    color: var(--text-primary);
    line-height: 1.6;
    min-height: 100vh;
}

/* Center the dashboard content */
.dashboard {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
}

What does this code do?

The :root block defines 10 CSS variables that control the entire color scheme. Every other style references these with var(--name) instead of hardcoding hex values.

The * selector removes default browser margins and padding. box-sizing: border-box makes padding count inside an element's width, so sizing is predictable.

The .dashboard container centers all content and caps it at 1200px wide so cards don't stretch too wide on large monitors.

  • Save style.css.
  • Check your browser. The page background should now be a deep dark blue-black, and all text should appear in a light off-white color.

Still seeing a white background?

Make sure your index.html file has <link rel="stylesheet" href="style.css"> in the <head> section. The filename must match exactly, including lowercase.

Also confirm you saved both files. Live Server only reloads when it detects a saved change.

Help me debug my CSS not loading.

Style the header and search input

The header needs to place the title on the left and the search bar on the right. The search input should look like it belongs in the dark theme, with a glow effect when focused.

  • Add the header, search input, and stats bar styles below your existing code in style.css:
/* Header - title left, search right */
.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1.5rem;
    padding-bottom: 1.5rem;
    border-bottom: 1px solid var(--border);
}

.header-content {
    display: flex;
    flex-direction: column;
}

.logo {
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--text-primary);
}

.subtitle {
    font-size: 0.875rem;
    color: var(--text-secondary);
    text-transform: uppercase;
    letter-spacing: 1px;
}

/* Search input with focus glow */
.search-container {
    flex-shrink: 0;
}

.search-input {
    width: 320px;
    padding: 0.75rem 1rem;
    background-color: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 0.95rem;
    outline: none;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.search-input::placeholder {
    color: var(--text-secondary);
}

.search-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.15);
}

/* Document count text */
.stats-bar {
    margin-bottom: 1.5rem;
    font-size: 0.875rem;
    color: var(--text-secondary);
}

What does this code do?

The .header uses flexbox with justify-content: space-between to push the title and search bar to opposite ends. A subtle bottom border separates the header from the content below.

The .search-input:focus rule adds a purple border and soft glow when the learner clicks into the search field. The transition property makes this change animate smoothly over 0.2 seconds.

  • Save style.css and check your browser.
  • You should see "Document Hub" on the left with "Declassified Archive" in small uppercase below it, and a dark search bar on the right.
  • Click into the search bar. A purple border and soft glow should appear.

Search bar not glowing on click?

Check that you included outline: none in the .search-input rule. Without it, the browser's default blue outline covers your custom focus style.

Help me fix my search input focus style.

Create the card grid and style each card

CSS Grid handles the multi-column card layout automatically. The auto-fill keyword tells the browser to fit as many columns as possible, wrapping cards to new rows as space shrinks.

  • Add the grid layout and card container styles below your existing code in style.css:
/* Card grid - auto-fills columns that are at least 300px wide */
.document-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 1.5rem;
}

/* Individual card styling */
.doc-card {
    background-color: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.5rem;
    transition: transform 0.2s, box-shadow 0.2s;
}

/* Lift card slightly on hover */
.doc-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow);
}

/* Hide cards that don't match a search (used by JavaScript later) */
.doc-card.hidden {
    display: none;
}

What does this code do?

The repeat(auto-fill, minmax(300px, 1fr)) line creates a grid where each column is at least 300px wide. If the screen is wide enough for three columns, you get three. Narrow the window and columns drop to two, then one.

The .doc-card:hover rule lifts the card 2 pixels upward and adds a shadow, giving tactile feedback when the user mouses over a card.

The .doc-card.hidden class will be toggled by JavaScript later to hide cards that do not match a search query.

  • Save style.css and check your browser.
  • You should see your 12 document cards arranged in a multi-column grid. Each card has a dark background and rounded corners.
  • Hover your mouse over any card. It should lift slightly and cast a shadow.

Cards stacked in a single column?

Your browser window might be too narrow for multiple 300px columns. Try widening the window. If cards still stack, check that .document-grid has display: grid and the grid-template-columns line is spelled exactly as shown.

Help me fix my CSS Grid layout.

  • Now add the card content typography and visibility styles below your existing code in style.css:
/* Document type pill badge */
.doc-type-badge {
    display: inline-block;
    padding: 0.25rem 0.75rem;
    background-color: rgba(108, 99, 255, 0.15);
    color: var(--accent);
    border-radius: 20px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 0.75rem;
}

/* Card text hierarchy */
.doc-title {
    font-size: 1.1rem;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 0.5rem;
    line-height: 1.3;
}

.doc-person {
    font-size: 0.9rem;
    color: var(--accent);
    font-weight: 500;
    margin-bottom: 0.25rem;
}

.doc-date {
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-bottom: 0.75rem;
}

.doc-description {
    font-size: 0.85rem;
    color: var(--text-secondary);
    line-height: 1.5;
}

/* No-results message (hidden by default, shown by JavaScript) */
.no-results {
    display: none;
    text-align: center;
    color: var(--text-secondary);
    font-size: 1rem;
    padding: 3rem 1rem;
}

.no-results.visible {
    display: block;
}

What does this code do?

The .doc-type-badge creates a pill-shaped label with a translucent purple background. It makes the document type (Report, Memo, Speech) stand out at the top of each card.

The text styles create a visual hierarchy. Titles are large and bright. Person names use the accent purple. Dates and descriptions are smaller and muted so they don't compete for attention.

The .no-results message stays hidden with display: none until JavaScript adds the visible class when a search has zero matches.

  • Save style.css and check your browser.
  • Each card should now show a purple pill badge at the top (like "REPORT" or "MEMO"), a bold white title, a purple person name, a muted date, and a light gray description.

Badge text not showing in purple?

Confirm the .doc-type-badge rule has color: var(--accent) (not --text-primary). Also check that your :root block at the top still defines --accent: #6c63ff.

Help me fix my card badge styling.

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css at this point. Compare yours against it:

:root {
    --bg-primary: #0f1117;
    --bg-secondary: #1a1d27;
    --bg-card: #232733;
    --text-primary: #e4e6eb;
    --text-secondary: #a0a3ab;
    --accent: #6c63ff;
    --accent-hover: #5a52d9;
    --border: #2e3241;
    --radius: 12px;
    --shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: var(--bg-primary);
    color: var(--text-primary);
    line-height: 1.6;
    min-height: 100vh;
}

.dashboard {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
}

.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1.5rem;
    padding-bottom: 1.5rem;
    border-bottom: 1px solid var(--border);
}

.header-content {
    display: flex;
    flex-direction: column;
}

.logo {
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--text-primary);
}

.subtitle {
    font-size: 0.875rem;
    color: var(--text-secondary);
    text-transform: uppercase;
    letter-spacing: 1px;
}

.search-container {
    flex-shrink: 0;
}

.search-input {
    width: 320px;
    padding: 0.75rem 1rem;
    background-color: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 0.95rem;
    outline: none;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.search-input::placeholder {
    color: var(--text-secondary);
}

.search-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.15);
}

.stats-bar {
    margin-bottom: 1.5rem;
    font-size: 0.875rem;
    color: var(--text-secondary);
}

.document-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 1.5rem;
}

.doc-card {
    background-color: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.5rem;
    transition: transform 0.2s, box-shadow 0.2s;
}

.doc-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow);
}

.doc-card.hidden {
    display: none;
}

.doc-type-badge {
    display: inline-block;
    padding: 0.25rem 0.75rem;
    background-color: rgba(108, 99, 255, 0.15);
    color: var(--accent);
    border-radius: 20px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 0.75rem;
}

.doc-title {
    font-size: 1.1rem;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 0.5rem;
    line-height: 1.3;
}

.doc-person {
    font-size: 0.9rem;
    color: var(--accent);
    font-weight: 500;
    margin-bottom: 0.25rem;
}

.doc-date {
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-bottom: 0.75rem;
}

.doc-description {
    font-size: 0.85rem;
    color: var(--text-secondary);
    line-height: 1.5;
}

.no-results {
    display: none;
    text-align: center;
    color: var(--text-secondary);
    font-size: 1rem;
    padding: 3rem 1rem;
}

.no-results.visible {
    display: block;
}

Your dashboard looks polished on a standard desktop screen. Next up, you'll add media queries so the layout adapts gracefully to tablets and phones.

Make It Responsive

Your dashboard looks great on a wide desktop screen, but try resizing your browser window narrower. The cards get cramped, the header overlaps, and the search bar disappears off the edge.

CSS media queries let you apply different styles depending on the screen size. In this step, you'll add two breakpoints that restructure the layout for tablets and phones.

In this step, get ready to:
  • Add a tablet breakpoint at 768px that stacks the header and expands the search bar.
  • Add a mobile breakpoint at 480px that switches to a single-column layout.
  • Test the responsive behavior by resizing your browser window.
Add the tablet breakpoint

A media query wraps a set of CSS rules that only activate when the screen meets a condition. The condition max-width: 768px targets screens 768 pixels wide or smaller, which covers most tablets in portrait orientation.

  • In style.css, scroll to the very bottom of the file (after the .no-results.visible rule).
  • Add the tablet media query by copying this code at the end of the file:
@media (max-width: 768px) {
    .dashboard {
        padding: 1.5rem;
    }

    .header {
        flex-direction: column;
        align-items: flex-start;
        gap: 1rem;
    }

    .search-input {
        width: 100%;
    }

    .document-grid {
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        gap: 1rem;
    }
}

What does this code do?

  • flex-direction: column stacks the header content and search bar vertically instead of side by side.
  • width: 100% on the search input makes it stretch the full width of the screen instead of staying fixed at 320px.
  • The grid's minmax drops from 300px to 250px, letting cards fit comfortably on narrower screens.
  • Save style.css.
  • Resize your browser window to roughly half your screen width. You should see the search bar jump below the title and stretch the full width of the page.

Search bar still stuck at 320px?

Make sure the @media block is placed after all other CSS rules in the file. If it appears before the .search-input rule, the original width: 320px overrides it.

Also check that the opening { and closing } braces match up. Each rule inside the media query has its own braces, and the entire media query has an outer pair.

Help me debug my media query

Add the mobile breakpoint

Tablets are covered, but phone screens (under 480px) need even more aggressive adjustments. On a phone, you want a single column of cards with tighter spacing so nothing feels cramped.

  • Directly below the 768px media query you just added, paste this second media query:
@media (max-width: 480px) {
    .dashboard {
        padding: 1rem;
    }

    .logo {
        font-size: 1.4rem;
    }

    .document-grid {
        grid-template-columns: 1fr;
        gap: 1rem;
    }

    .doc-card {
        padding: 1.25rem;
    }
}

What does this code do?

  • grid-template-columns: 1fr forces a single-column layout. Every card takes the full width of the screen.
  • The logo font size shrinks from 1.75rem to 1.4rem so the title doesn't dominate a small screen.
  • Card padding reduces slightly to give the text more breathing room within the narrower card width.
  • Save style.css.
  • Drag your browser window very narrow (around phone width). You should see the cards stack into a single column with one card per row.

Cards still showing two columns on a narrow window?

Check that you wrote max-width: 480px and not min-width. Using min-width activates the styles on large screens instead of small ones.

Help me fix my mobile breakpoint

Test the responsive behavior

Both breakpoints are in place. Time to see them work together as a smooth, continuous adaptation from desktop down to mobile.

  • Make your browser window full width. You should see 3 columns of cards with the search bar on the right of the header.
  • Slowly drag the right edge of the browser window toward the left.
  • Around 768px wide, the header stacks vertically and the search bar stretches full width.
  • Keep dragging narrower. Around 480px, the grid snaps to a single column and the logo text gets smaller.

Why two separate breakpoints?

Each breakpoint targets a specific device category. The 768px breakpoint covers tablets where users still have some width to work with. The 480px breakpoint handles phones where space is extremely limited.

Between breakpoints, the CSS Grid's auto-fill and minmax() handle the in-between sizes automatically. Cards wrap to fewer columns as space runs out, without needing a breakpoint for every possible width.

✔️ Awesome, I've got everything!

Great. Double check you've saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css file including both media queries at the bottom:

:root {
    --bg-primary: #0f1117;
    --bg-secondary: #1a1d27;
    --bg-card: #232733;
    --text-primary: #e4e6eb;
    --text-secondary: #a0a3ab;
    --accent: #6c63ff;
    --accent-hover: #5a52d9;
    --border: #2e3241;
    --radius: 12px;
    --shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: var(--bg-primary);
    color: var(--text-primary);
    line-height: 1.6;
    min-height: 100vh;
}

.dashboard {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
}

.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1.5rem;
    padding-bottom: 1.5rem;
    border-bottom: 1px solid var(--border);
}

.header-content {
    display: flex;
    flex-direction: column;
}

.logo {
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--text-primary);
}

.subtitle {
    font-size: 0.875rem;
    color: var(--text-secondary);
    text-transform: uppercase;
    letter-spacing: 1px;
}

.search-container {
    flex-shrink: 0;
}

.search-input {
    width: 320px;
    padding: 0.75rem 1rem;
    background-color: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 0.95rem;
    outline: none;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.search-input::placeholder {
    color: var(--text-secondary);
}

.search-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.15);
}

.stats-bar {
    margin-bottom: 1.5rem;
    font-size: 0.875rem;
    color: var(--text-secondary);
}

.document-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 1.5rem;
}

.doc-card {
    background-color: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.5rem;
    transition: transform 0.2s, box-shadow 0.2s;
}

.doc-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow);
}

.doc-card.hidden {
    display: none;
}

.doc-type-badge {
    display: inline-block;
    padding: 0.25rem 0.75rem;
    background-color: rgba(108, 99, 255, 0.15);
    color: var(--accent);
    border-radius: 20px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 0.75rem;
}

.doc-title {
    font-size: 1.1rem;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 0.5rem;
    line-height: 1.3;
}

.doc-person {
    font-size: 0.9rem;
    color: var(--accent);
    font-weight: 500;
    margin-bottom: 0.25rem;
}

.doc-date {
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-bottom: 0.75rem;
}

.doc-description {
    font-size: 0.85rem;
    color: var(--text-secondary);
    line-height: 1.5;
}

.no-results {
    display: none;
    text-align: center;
    color: var(--text-secondary);
    font-size: 1rem;
    padding: 3rem 1rem;
}

.no-results.visible {
    display: block;
}

@media (max-width: 768px) {
    .dashboard {
        padding: 1.5rem;
    }

    .header {
        flex-direction: column;
        align-items: flex-start;
        gap: 1rem;
    }

    .search-input {
        width: 100%;
    }

    .document-grid {
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        gap: 1rem;
    }
}

@media (max-width: 480px) {
    .dashboard {
        padding: 1rem;
    }

    .logo {
        font-size: 1.4rem;
    }

    .document-grid {
        grid-template-columns: 1fr;
        gap: 1rem;
    }

    .doc-card {
        padding: 1.25rem;
    }
}

Your dashboard now adapts beautifully from a wide desktop monitor down to a phone screen. Next up, you'll bring it to life with real-time search filtering using JavaScript.

Add Real-Time Search Filtering

Your dashboard looks polished and adapts smoothly to any screen size. The one thing missing is interactivity. That search bar at the top is just sitting there, doing nothing.

In this step, you'll write JavaScript that listens for every keystroke in the search field. As the user types, cards that don't match will vanish instantly, and the document count will update in real time.

In this step, get ready to:
  • Select the DOM elements your JavaScript needs to interact with.
  • Add a real-time event listener that responds to every keystroke in the search bar.
  • Show a "no results" message when no documents match the search query.
Select the DOM elements

Before JavaScript can change anything on the page, it needs references to the elements it will control. You'll grab the search input, the card grid, each individual card, the count display, and the no-results message.

  • In VS Code, switch to script.js (currently empty) and add the following DOM selection code:
const searchInput = document.getElementById('search-input');
const documentGrid = document.getElementById('document-grid');
const docCount = document.getElementById('doc-count');
const noResults = document.getElementById('no-results');
const cards = documentGrid.querySelectorAll('.doc-card');
const totalDocs = cards.length;

What does this code do?

Each line grabs one element from the HTML using its ID or class name, then stores it in a variable for later use.

  • searchInput is the text field the user types in.
  • cards is a list of all 12 document card elements on the page.
  • totalDocs stores the number 12, which you'll use to display the total count.

You won't see a change in the browser yet. These lines just prepare the variables that the next chunk will use.

Add the search event listener

An event listener watches for a specific action (like typing) and runs code every time that action happens. You'll attach one to the search input that fires on every keystroke, paste, or delete.

  • In script.js, add the following event listener code below the variables you just wrote:
searchInput.addEventListener('input', function () {
    const query = this.value.trim().toLowerCase();
    let visibleCount = 0;

    cards.forEach(function (card) {
        // Get all text inside this card and the person data attribute
        const textContent = card.textContent.toLowerCase();
        const person = card.getAttribute('data-person');

        // Check if the query matches the card's content or person name
        const matchesSearch = query === '' || textContent.includes(query) || person.includes(query);

        if (matchesSearch) {
            card.classList.remove('hidden');
            visibleCount++;
        } else {
            card.classList.add('hidden');
        }
    });

    // Update the document count display
    docCount.textContent = 'Showing ' + visibleCount + ' of ' + totalDocs + ' documents';

    // Show a no-results message when nothing matches
    if (visibleCount === 0 && query !== '') {
        noResults.classList.add('visible');
    } else {
        noResults.classList.remove('visible');
    }
});

What does this code do?

  • The input event fires every time the search field changes. This covers typing, pasting, and deleting text.
  • this.value.trim().toLowerCase() reads the current text, strips extra spaces, and converts to lowercase so the search is case-insensitive.
  • For each card, it grabs all text inside plus the data-person attribute, then checks whether the query appears anywhere in either string.
  • Matching cards get the hidden class removed (visible). Non-matching cards get it added (disappear).
  • After the loop, the stats bar updates with the new count, and a "no results" paragraph appears if zero cards match.

Why use textContent instead of innerText?

textContent captures every piece of text inside the card element, including the title, person, date, and description. This means a single check covers all the card's searchable content without needing to target each field individually.

  • Save script.js.
  • Switch to Chrome. Click inside the search bar and type Kennedy. You should see only the three John F. Kennedy cards remain, and the stats bar should read "Showing 3 of 12 documents".

Nothing happens when you type?

  • Open DevTools with Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows) and check the Console tab for red error messages. A typo in an element ID (like search-Input instead of search-input) will cause a "Cannot read properties of null" error.
  • Make sure your <script src="script.js"></script> tag is at the bottom of index.html, just before </body>. If it's in the <head>, the elements don't exist yet when the script runs.
  • Confirm that your HTML cards have class="doc-card" on each <article> element. The selector .doc-card won't find cards with a different class name.

Help me debug why my search filtering isn't working in Document Hub.

Test the real-time search

The search is live. Let's make sure it handles all the scenarios you built it for.

  • Clear the search bar completely. All 12 cards should reappear and the count should reset to "Showing 12 of 12 documents".
  • Type einstein. You should see two cards (Einstein-Szilard Letter and FBI Surveillance File Summary).
  • Type xyzabc (something that matches nothing). You should see zero cards and the message "No documents found. Try a different search term." below the empty grid.
  • Type speech. You should see five cards since five documents contain the word "Speech" in their badge text.

Why does searching "JFK" work?

The search checks each card's full textContent, which includes the title, person name, date, and description. It also checks the data-person attribute separately. Typing a person's name surfaces every document associated with them.

✔️ Awesome, I've got everything!

Great. Double check you've saved script.js and confirmed all four test scenarios work.

ⓧ I'd like to double check the full code

Here is the complete script.js file:

const searchInput = document.getElementById('search-input');
const documentGrid = document.getElementById('document-grid');
const docCount = document.getElementById('doc-count');
const noResults = document.getElementById('no-results');
const cards = documentGrid.querySelectorAll('.doc-card');
const totalDocs = cards.length;

searchInput.addEventListener('input', function () {
    const query = this.value.trim().toLowerCase();
    let visibleCount = 0;

    cards.forEach(function (card) {
        const textContent = card.textContent.toLowerCase();
        const person = card.getAttribute('data-person');

        const matchesSearch = query === '' || textContent.includes(query) || person.includes(query);

        if (matchesSearch) {
            card.classList.remove('hidden');
            visibleCount++;
        } else {
            card.classList.add('hidden');
        }
    });

    docCount.textContent = 'Showing ' + visibleCount + ' of ' + totalDocs + ' documents';

    if (visibleCount === 0 && query !== '') {
        noResults.classList.add('visible');
    } else {
        noResults.classList.remove('visible');
    }
});

Your Document Hub is fully functional. You've built a responsive, interactive dashboard from scratch using only HTML, CSS, and JavaScript. Next up, the Secret Mission challenges you to add category filter tags that combine with the search bar for even more powerful filtering.

Secret mission

Add Category Filter Tags

Your search bar filters by text, but what if users want to browse by document type? Add clickable category tags that combine with the search bar for multi-criteria filtering.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The document-hub project 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're still actively building or want to experiment with the dashboard further.

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

Pause - I'll come back to this later

Stop the Live Server to free up the local port, but keep your files for later.

  • In VS Code, click Port: 5500 in the bottom status bar to stop Live Server.
  • Your document-hub folder stays on your Desktop. You can relaunch Live Server any time by right-clicking index.html and selecting Open with Live Server.

Delete - I don't want to use this again

Remove all project files and extensions to start fresh.

  • Stop Live Server if it is still running by clicking Port: 5500 in the VS Code status bar.
  • Delete the project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/document-hub

Windows

rmdir /s /q %USERPROFILE%\Desktop\document-hub
  • To uninstall the Live Server extension, open the Extensions panel in VS Code (Cmd+Shift+X on macOS or Ctrl+Shift+X on Windows).
  • Search for Live Server, click on the extension, then click Uninstall.

Nice Work!

Nice Work!

Nice work! You just built a fully interactive document hub dashboard using nothing but HTML, CSS, and JavaScript.

You've learned how to:

  • Created a dark-themed design system using CSS custom properties to define reusable color tokens across an entire stylesheet.
  • Implemented a responsive CSS Grid layout that adapts from a multi-column desktop view to a single-column mobile view using media queries.
  • Wrote vanilla JavaScript that filters DOM elements in real time based on user input, connecting person names to documents so searching a name surfaces all related files.
  • Secret Mission: Added interactive category filter tags that combine with text search for multi-criteria filtering.

Ready to quiz yourself?