Build a Touch-Friendly Document Hub
Build a dark-themed document hub with search, preview, and live deployment.
Introduction
30 Second Summary
Think about how you browse files on your phone. The best experiences have big tap targets, instant feedback, and layouts built for fingertips instead of mouse cursors.
In this project, you will build a touch-optimized document hub website with a modern dark theme, real-time search, and an inline file preview panel. You will deploy it live on the internet for free using Cloudflare Pages.
What You'll Build
A touch-optimized website built with HTML, CSS Grid, and vanilla JavaScript that organizes files into tappable categories with real-time search filtering and an embedded preview panel.
By the end of this project, you'll have:
- A responsive CSS Grid layout with a search bar, categorized file sections, and a preview panel styled in a modern dark theme.
- A real-time search and category filter system built with vanilla JavaScript that lets you find and browse files instantly.
- A live deployment on Cloudflare Pages accessible from any device with an internet connection.
- Secret Mission: Add a light mode toggle with smooth CSS transitions and localStorage persistence.
Are there any prerequisites?
You should have basic HTML and CSS knowledge before starting this project. No JavaScript experience is required. You will learn everything you need as you go.
Before We Start
Every project starts with a solid foundation. Before you write any code, you need a workspace on your computer and a place to host your finished site on the internet.
In this step, you will create your project folder structure in VS Code, sign up for a free Cloudflare Pages account (your deployment platform), and add some sample files to test with later.
In this step, get ready to:
- Create the project folder with HTML, CSS, and JS files.
- Set up a free Cloudflare account for deployment.
- Add sample files to test your document hub with.
Create your project folder and files
Your document hub needs three core files: one for structure (HTML), one for styling (CSS), and one for interactivity (JavaScript). You will create all of them inside a single project folder.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (the backtick key, usually above Tab).
What is the integrated terminal?
The integrated terminal is a command-line window built into VS Code. It lets you run commands without leaving your editor.
- 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 this command:
macOS
touch index.html style.css script.js
Windows
New-Item index.html, style.css, script.js
- Create a subfolder for your sample files by running:
mkdir files
- Confirm everything was created by running:
ls
You should see index.html, style.css, script.js, and a files folder listed in the output.
Don't see your files?
Make sure you ran cd document-hub before creating the files. If you accidentally created them on the Desktop, delete them and start again from the mkdir document-hub step.
help me troubleshoot my folder structure
- Open the document-hub folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the document-hub folder, and click Select Folder.
The Explorer sidebar on the left should now show your three files and the files folder.
Create a free Cloudflare account
Cloudflare Pages is where your finished site will live on the internet. The free tier includes unlimited bandwidth and 500 builds per month, so your site can stay live forever at no cost.
- Open your browser and go to https://dash.cloudflare.com/sign-up.
- Enter your email address and create a password.
- Click Sign up.
- Check your email inbox for a verification message from Cloudflare.
- Click the verification link in the email to confirm your account.
- After verifying, log in to the Cloudflare dashboard at https://dash.cloudflare.com.
You should see the Cloudflare dashboard homepage after logging in.
Didn't receive the verification email?
Check your spam or junk folder. The email comes from Cloudflare and may take a minute to arrive. If it still does not appear, click the resend link on the Cloudflare verification page.
help me get verified on Cloudflare
Add sample files for testing
Your document hub will preview different file types (images, PDFs, text). You need at least two sample files inside the files folder to test with.
- In VS Code, right-click the files folder in the Explorer sidebar.
- Select New File and name it sample.txt.
- Type a few lines of placeholder text inside sample.txt (anything works, such as "This is a sample document for testing the preview panel.").
- Save sample.txt by pressing Ctrl+S (Windows) or Cmd+S (macOS).
Next, you need a sample image file.
- Open your browser and find any small image (a photo, an icon, or a simple graphic).
- Right-click the image and select Save image as.
- Navigate to your Desktop/document-hub/files folder.
- Save the image as sample-image.jpg.
Any image works
The image content does not matter for testing. A small photo or screenshot works perfectly. Just make sure you save it with the exact name sample-image.jpg so it matches the code you will write later.
- Back in VS Code, expand the files folder in the Explorer sidebar.
- Confirm you can see both sample.txt and sample-image.jpg listed inside it.
Your workspace is ready. Next up, you will build the HTML structure that turns this empty folder into a real document hub.
Build the HTML Structure
Your project folder is ready and your Cloudflare account is set up. Now it's time to build the bones of your document hub.
Every website starts with an HTML skeleton that defines the layout and content areas. In this step, you'll create the full page structure (search bar, categorized file lists, and preview panel) and define the file data that powers the interface.
In this step, get ready to:
- Build the full HTML page skeleton with semantic sections for search, files, and preview.
- Define file data in JavaScript so the hub knows what to display.
Set up the HTML document with the page skeleton
The HTML file defines every section of the document hub's interface. You'll create a header with a search input, a main area with three file category sections, and a bottom panel split between a preview area and a profile section.
- In VS Code, click on index.html in the Explorer sidebar to open it.
- Paste the full HTML structure by copying this code into the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, interactive-widget=resizes-content">
<title>Document Hub</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<header class="header">
<div class="search-bar">
<input type="search" id="searchInput" placeholder="Search files..." aria-label="Search files">
</div>
</header>
<main class="file-list">
<section class="category" data-category="documents">
<button class="category-heading" aria-expanded="true">Documents / PDFs / txt / doc</button>
<ul class="category-items" id="documents-list"></ul>
</section>
<section class="category" data-category="videos">
<button class="category-heading" aria-expanded="true">Videos</button>
<ul class="category-items" id="videos-list"></ul>
</section>
<section class="category" data-category="images">
<button class="category-heading" aria-expanded="true">Images</button>
<ul class="category-items" id="images-list"></ul>
</section>
</main>
<section class="bottom-panel">
<div class="preview-panel" id="previewPanel">
<p class="preview-placeholder">Tap a file to preview it here</p>
</div>
<div class="profile-panel">
<div class="profile-picture" aria-label="Profile picture"></div>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>
What does this code do?
- The <meta name="viewport"> tag with user-scalable=no disables pinch-to-zoom, which is common for touch-first kiosk interfaces. The large 48px touch targets throughout the design keep accessibility intact.
- The .app container wraps the entire interface. Inside it, a <header> holds the search bar, a <main> element holds the three category sections, and a <section> at the bottom holds the preview and profile panels.
- Each category uses a <button> for its heading (making it tappable) and an empty <ul> that JavaScript will fill with file items later.
- The <link> and <script> tags connect your CSS and JavaScript files so they load with the page.
- Save index.html by pressing Ctrl+S.
- Open the file in your browser by right-clicking index.html in File Explorer, selecting Open with, then choosing Google Chrome (or your preferred browser).
- You should see the text "Search files..." in a search input at the top, three category headings (Documents / PDFs / txt / doc, Videos, Images), placeholder text "Tap a file to preview it here", and a blank profile circle area. Everything will look unstyled and plain.
Seeing a blank page?
- Make sure you saved the file after pasting the code. Unsaved changes show a dot on the file tab in VS Code.
- Check that you opened index.html directly (not the folder). The browser URL should start with file:/// followed by the path to your file.
Help me troubleshoot why my HTML page is blank in the browser.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ 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, user-scalable=no, interactive-widget=resizes-content">
<title>Document Hub</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<header class="header">
<div class="search-bar">
<input type="search" id="searchInput" placeholder="Search files..." aria-label="Search files">
</div>
</header>
<main class="file-list">
<section class="category" data-category="documents">
<button class="category-heading" aria-expanded="true">Documents / PDFs / txt / doc</button>
<ul class="category-items" id="documents-list"></ul>
</section>
<section class="category" data-category="videos">
<button class="category-heading" aria-expanded="true">Videos</button>
<ul class="category-items" id="videos-list"></ul>
</section>
<section class="category" data-category="images">
<button class="category-heading" aria-expanded="true">Images</button>
<ul class="category-items" id="images-list"></ul>
</section>
</main>
<section class="bottom-panel">
<div class="preview-panel" id="previewPanel">
<p class="preview-placeholder">Tap a file to preview it here</p>
</div>
<div class="profile-panel">
<div class="profile-picture" aria-label="Profile picture"></div>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>
Add the file data as a JavaScript array
Your HTML has empty lists waiting to be filled. The JavaScript file will hold the data for every file in your hub, organized by category.
- In VS Code, click on script.js in the Explorer sidebar to open it.
- Define the file data array and icon lookup by pasting this code:
const files = [
{ name: "Project Report.pdf", category: "documents", type: "pdf", path: "files/sample.txt" },
{ name: "Meeting Notes.docx", category: "documents", type: "docx", path: "files/sample.txt" },
{ name: "README.txt", category: "documents", type: "txt", path: "files/sample.txt" },
{ name: "Training Video.mp4", category: "videos", type: "mp4", path: "" },
{ name: "Product Demo.mp4", category: "videos", type: "mp4", path: "" },
{ name: "Team Photo.jpg", category: "images", type: "jpg", path: "files/sample-image.jpg" },
{ name: "Logo.png", category: "images", type: "png", path: "files/sample-image.jpg" },
{ name: "Diagram.png", category: "images", type: "png", path: "files/sample-image.jpg" }
];
const fileIcons = {
pdf: "📄",
docx: "📝",
txt: "📃",
mp4: "🎬",
jpg: "🖼️",
png: "🖼️"
};
What does this code do?
- The files array holds 8 objects, each representing a file in the hub. Every object has four properties: name (display label), category (which section it belongs to), type (file extension), and path (location of the actual file for preview).
- Document paths point to files/sample.txt and image paths point to files/sample-image.jpg. These are the sample files you added in Step 1.
- The fileIcons object maps each file type to an emoji icon. Later, JavaScript will use this to display the right icon next to each file name.
- Save script.js by pressing Ctrl+S.
- Switch to your browser and refresh the page.
- Press F12 to open Developer Tools, then click the Console tab.
- Type files and press Enter. You should see an array of 8 objects logged in the console.
Console shows 'files is not defined'?
- Make sure you saved script.js before refreshing the browser.
- Check that the <script src="script.js"></script> line exists at the bottom of your index.html file, just before </body>.
- Confirm that script.js is in the same folder as index.html (both inside document-hub).
Help me fix a 'files is not defined' error in my browser console.
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js before moving on.
ⓧ I'd like to double check the full code
const files = [
{ name: "Project Report.pdf", category: "documents", type: "pdf", path: "files/sample.txt" },
{ name: "Meeting Notes.docx", category: "documents", type: "docx", path: "files/sample.txt" },
{ name: "README.txt", category: "documents", type: "txt", path: "files/sample.txt" },
{ name: "Training Video.mp4", category: "videos", type: "mp4", path: "" },
{ name: "Product Demo.mp4", category: "videos", type: "mp4", path: "" },
{ name: "Team Photo.jpg", category: "images", type: "jpg", path: "files/sample-image.jpg" },
{ name: "Logo.png", category: "images", type: "png", path: "files/sample-image.jpg" },
{ name: "Diagram.png", category: "images", type: "png", path: "files/sample-image.jpg" }
];
const fileIcons = {
pdf: "📄",
docx: "📝",
txt: "📃",
mp4: "🎬",
jpg: "🖼️",
png: "🖼️"
};
Your HTML skeleton and file data are locked in. Next up, you'll bring this layout to life with CSS Grid and a modern dark theme.
Style the Layout with CSS Grid
Your HTML structure is in place, but right now it looks like plain unstyled text on a white background. To transform this into a polished, touch-friendly interface, you need a layout system that handles both rows and columns at the same time.
CSS Grid is the perfect tool here because your design has a 2D structure: a full-width search bar on top, file categories in the middle, and a split bottom row with a preview panel on the left and a profile area on the right. In this step, you will build out the entire stylesheet with a modern dark theme, a CSS Grid layout, and touch-optimized interaction states.
In this step, get ready to:
- Set up a CSS reset and dark theme with custom properties.
- Build the main grid layout with styled search bar, file categories, preview panel, and profile area.
- Add responsive behavior and touch feedback states.
Set up the CSS reset and modern dark theme
Before any layout work, you need a clean slate. Browser defaults add inconsistent margins, padding, and box-sizing that break precise layouts. You also need a central color palette so you can change the theme in one place later.
- Switch back to style.css in VS Code and add the CSS reset, custom properties, and body styles by pasting the following code:
/* Reset browser defaults for consistent rendering */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Dark theme color palette as custom properties */
:root {
--bg-primary: #2b2d42;
--bg-secondary: #3d3f56;
--bg-header: #4a4e69;
--text-primary: #edf2f4;
--text-secondary: #a8b2c1;
--accent: #6c9fff;
--border: #525672;
--item-hover: #4a4e69;
--item-active: #5c6188;
--radius: 8px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--transition: 0.2s ease;
}
/* Base page styles with touch optimization */
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 16px;
color: var(--text-primary);
background-color: var(--bg-primary);
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
What does this code do?
The universal selector reset (*, *::before, *::after) forces every element to use border-box sizing and strips default margins. This means padding and borders are included in an element's width, so layout math stays predictable.
The :root block defines CSS custom properties (variables) for every color, spacing value, and transition you will reuse. Changing a single variable here updates every element that references it.
On the body, touch-action: manipulation eliminates the 300ms tap delay and prevents accidental double-tap zoom, while still allowing normal scrolling. The -webkit-tap-highlight-color: transparent removes the blue flash that appears on tap in mobile browsers.
- Save style.css.
- Open index.html in your browser (double-click the file, or right-click and select **Open with** your browser). You should see a dark background (#2b2d42) with light text, no margins, and no default bullet points on the lists.
Still seeing a white background?
- Confirm that style.css is saved in the same folder as index.html. The <link> tag in your HTML references href="style.css" without any folder path.
- Hard-refresh the page with **Ctrl+Shift+R** (Windows) or **Cmd+Shift+R** (macOS) to bypass the browser cache.
Why is my page still showing a white background?
Build the main grid layout matching the design
With the dark theme in place, it is time to arrange the page into its final layout. The design calls for three horizontal bands: a header row (search bar), a flexible middle row (file categories), and a bottom row split into two columns (preview panel and profile).
Why CSS Grid instead of Flexbox?
Flexbox works in one direction at a time (row or column). Your layout needs both rows AND columns, with a bottom section that splits horizontally. CSS Grid handles this natively without extra wrapper elements.
- Add the app container grid, header, and search bar styles below your existing code in style.css:
/* Main app grid: header on top, file list fills middle, bottom panel at base */
.app {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100vh;
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-md);
gap: var(--spacing-md);
}
/* Header bar containing the search input */
.header {
background-color: var(--bg-header);
border-radius: var(--radius);
padding: var(--spacing-md);
}
/* Search input styled for easy finger tapping */
.search-bar input {
width: 100%;
min-height: 48px;
padding: var(--spacing-sm) var(--spacing-md);
font-size: 1rem;
border: 2px solid var(--border);
border-radius: var(--radius);
background-color: var(--bg-secondary);
color: var(--text-primary);
outline: none;
transition: border-color var(--transition);
}
.search-bar input:focus {
border-color: var(--accent);
}
.search-bar input::placeholder {
color: var(--text-secondary);
}
What does this code do?
- grid-template-rows: auto 1fr auto creates three rows. The header and bottom panel take only the space they need (auto), while the file list expands to fill all remaining space (1fr).
- min-height: 48px on the search input ensures the touch target meets the 44px accessibility minimum, with a bit of breathing room.
- The :focus state highlights the input border with the accent color so learners know the field is active.
- Save style.css and refresh your browser. You should see the search bar sitting inside a rounded blue-gray header at the top, with the rest of the content stacking below it in the dark theme.
Search bar not filling the header width?
- Check that your selector is .search-bar input (with a space), not .search-bar-input. The space means "an input inside .search-bar".
- Verify that width: 100% is present on the input rule.
Help me figure out why my search bar CSS isn't working.
- Next, add the file list and category styles below the search bar code in style.css:
/* Scrollable container for all category sections */
.file-list {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
/* Each category card */
.category {
background-color: var(--bg-secondary);
border-radius: var(--radius);
overflow: hidden;
}
/* Category heading buttons with 48px touch target */
.category-heading {
display: block;
width: 100%;
min-height: 48px;
padding: var(--spacing-sm) var(--spacing-md);
font-size: 1rem;
font-weight: 600;
text-align: left;
background-color: var(--bg-header);
border: none;
color: var(--text-primary);
cursor: pointer;
transition: background-color var(--transition);
}
/* Hide items when category is collapsed */
.category-heading[aria-expanded="false"] + .category-items {
display: none;
}
.category-items {
list-style: none;
padding: var(--spacing-sm);
}
/* Individual file items with 48px touch targets */
.file-item {
display: flex;
align-items: center;
min-height: 48px;
padding: var(--spacing-sm) var(--spacing-md);
margin-bottom: 4px;
border-radius: var(--radius);
cursor: pointer;
transition: background-color var(--transition), transform 0.1s ease;
color: var(--text-primary);
}
.file-item-icon {
margin-right: var(--spacing-sm);
font-size: 1.25rem;
}
What does this code do?
- overflow-y: auto on .file-list makes the middle section scroll independently if files overflow the available space.
- The CSS selector .category-heading[aria-expanded="false"] + .category-items uses the adjacent sibling combinator. When JavaScript toggles the attribute to false, the list hides automatically with pure CSS.
- Every interactive element (.category-heading, .file-item) has min-height: 48px so fingers can tap them comfortably on a touchscreen.
- Save style.css and refresh your browser. You should see three rounded category cards stacked vertically in the middle of the page, each with a darker heading bar showing the category name.
Categories not stacking properly?
- Make sure there is no typo in flex-direction: column on the .file-list rule. Without it, the categories sit side by side.
- Check that the class name in CSS (.file-list) matches the HTML (class="file-list") exactly.
Why aren't my category sections stacking vertically?
- Complete the layout by adding the bottom panel styles below your existing code in style.css:
/* Bottom section: preview panel (left) and profile (right) */
.bottom-panel {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--spacing-md);
min-height: 200px;
}
/* Preview area with centered placeholder text */
.preview-panel {
background-color: var(--bg-secondary);
border: 2px solid var(--border);
border-radius: var(--radius);
padding: var(--spacing-md);
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
}
.preview-placeholder {
color: var(--text-secondary);
font-style: italic;
}
/* Constrain preview media to fit the panel */
.preview-panel img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: var(--radius);
}
.preview-panel iframe {
width: 100%;
height: 100%;
border: none;
border-radius: var(--radius);
}
.preview-panel video {
max-width: 100%;
max-height: 100%;
border-radius: var(--radius);
}
/* Profile section with circular avatar placeholder */
.profile-panel {
width: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.profile-picture {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: var(--bg-header);
border: 2px solid var(--border);
}
What does this code do?
- grid-template-columns: 1fr auto on .bottom-panel gives the preview panel all available width (1fr) while the profile area only takes the space it needs (auto).
- The preview media rules (img, iframe, video) constrain content to fit inside the panel without overflowing or distorting.
- border-radius: 50% on .profile-picture turns the square div into a circle, creating a profile avatar placeholder.
- Save style.css and refresh your browser.
- You should now see the full layout: a search bar at the top, category cards in the middle, a bordered preview panel on the bottom-left with italic placeholder text, and a circular profile picture placeholder on the bottom-right. Everything uses the dark theme.
Preview panel and profile not side by side?
- Make sure .bottom-panel has display: grid and grid-template-columns: 1fr auto. Without both properties, the panels stack vertically.
- Verify the class name in your HTML is bottom-panel (with a hyphen, not an underscore or space).
Help me fix my bottom panel grid layout.
Add responsive behavior and touch feedback
A touch-friendly interface needs immediate visual feedback when you tap something. Without it, users are unsure whether their tap registered. You also need the layout to adapt gracefully on smaller screens where the side-by-side bottom panels would be too cramped.
- Add the interaction states and responsive media query at the bottom of style.css:
/* Touch feedback: highlight on tap */
.category-heading:active {
background-color: var(--item-active);
}
.file-item:active {
background-color: var(--item-active);
transform: scale(0.98);
}
.file-item:hover {
background-color: var(--item-hover);
}
.file-item.selected {
background-color: var(--item-active);
font-weight: 600;
}
/* Stack bottom panels on narrow screens */
@media (max-width: 600px) {
.bottom-panel {
grid-template-columns: 1fr;
}
.profile-panel {
width: 100%;
justify-content: center;
padding: var(--spacing-md) 0;
}
}
What does this code do?
- The :active pseudo-class fires the moment a finger touches the element. The transform: scale(0.98) gives a subtle "press" animation that mimics a physical button being pushed.
- The .selected class will be added by JavaScript later when a file is tapped. It stays highlighted until another file is selected.
- The media query at max-width: 600px switches the bottom panel from two columns to a single stacked column on phone-sized screens.
- Save style.css and refresh your browser.
- Click and hold on a category heading or any area where file items will appear. You should see the background color lighten briefly while your mouse is held down.
- Resize your browser window to a narrow width (below 600px). The preview panel and profile area should stack vertically instead of sitting side by side.
No visible change when clicking?
- The :active state only shows while you hold the mouse button down. Click and hold (do not release immediately) to see the color change.
- Make sure the background-color values in your :active rules use var(--item-active), which is lighter than the base background.
Why isn't my CSS :active state showing any visual change?
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css and the page looks correct with all sections styled.
ⓧ I'd like to double check the full code
Here is the complete style.css file at the end of this step. Compare it to yours and fix any differences.
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg-primary: #2b2d42;
--bg-secondary: #3d3f56;
--bg-header: #4a4e69;
--text-primary: #edf2f4;
--text-secondary: #a8b2c1;
--accent: #6c9fff;
--border: #525672;
--item-hover: #4a4e69;
--item-active: #5c6188;
--radius: 8px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--transition: 0.2s ease;
}
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 16px;
color: var(--text-primary);
background-color: var(--bg-primary);
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.app {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100vh;
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-md);
gap: var(--spacing-md);
}
.header {
background-color: var(--bg-header);
border-radius: var(--radius);
padding: var(--spacing-md);
}
.search-bar input {
width: 100%;
min-height: 48px;
padding: var(--spacing-sm) var(--spacing-md);
font-size: 1rem;
border: 2px solid var(--border);
border-radius: var(--radius);
background-color: var(--bg-secondary);
color: var(--text-primary);
outline: none;
transition: border-color var(--transition);
}
.search-bar input:focus {
border-color: var(--accent);
}
.search-bar input::placeholder {
color: var(--text-secondary);
}
.file-list {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.category {
background-color: var(--bg-secondary);
border-radius: var(--radius);
overflow: hidden;
}
.category-heading {
display: block;
width: 100%;
min-height: 48px;
padding: var(--spacing-sm) var(--spacing-md);
font-size: 1rem;
font-weight: 600;
text-align: left;
background-color: var(--bg-header);
border: none;
color: var(--text-primary);
cursor: pointer;
transition: background-color var(--transition);
}
.category-heading:active {
background-color: var(--item-active);
}
.category-heading[aria-expanded="false"] + .category-items {
display: none;
}
.category-items {
list-style: none;
padding: var(--spacing-sm);
}
.file-item {
display: flex;
align-items: center;
min-height: 48px;
padding: var(--spacing-sm) var(--spacing-md);
margin-bottom: 4px;
border-radius: var(--radius);
cursor: pointer;
transition: background-color var(--transition), transform 0.1s ease;
color: var(--text-primary);
}
.file-item:active {
background-color: var(--item-active);
transform: scale(0.98);
}
.file-item:hover {
background-color: var(--item-hover);
}
.file-item.selected {
background-color: var(--item-active);
font-weight: 600;
}
.file-item-icon {
margin-right: var(--spacing-sm);
font-size: 1.25rem;
}
.bottom-panel {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--spacing-md);
min-height: 200px;
}
.preview-panel {
background-color: var(--bg-secondary);
border: 2px solid var(--border);
border-radius: var(--radius);
padding: var(--spacing-md);
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
}
.preview-placeholder {
color: var(--text-secondary);
font-style: italic;
}
.preview-panel img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: var(--radius);
}
.preview-panel iframe {
width: 100%;
height: 100%;
border: none;
border-radius: var(--radius);
}
.preview-panel video {
max-width: 100%;
max-height: 100%;
border-radius: var(--radius);
}
.profile-panel {
width: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.profile-picture {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: var(--bg-header);
border: 2px solid var(--border);
}
@media (max-width: 600px) {
.bottom-panel {
grid-template-columns: 1fr;
}
.profile-panel {
width: 100%;
justify-content: center;
padding: var(--spacing-md) 0;
}
}
Your document hub now has a polished, dark-themed layout that is ready for finger-friendly interaction. Next up, you will bring it to life with JavaScript that renders files dynamically, powers the search bar, and enables tap-to-preview.
Add Search, Filtering, and Preview with JavaScript
Your document hub has a polished dark-themed layout, but right now it is completely static. The category lists are empty, the search bar does nothing, and the preview panel just shows placeholder text.
In this step, you will bring the hub to life with vanilla JavaScript. You will dynamically render files into their categories, wire up real-time search filtering, and build a preview panel that responds to taps.
In this step, get ready to:
- Render file items dynamically and add category accordion toggles.
- Implement real-time search filtering across all categories.
- Build an inline preview panel that displays images, PDFs, and videos.
Render the file list dynamically and wire up category filtering
The HTML has empty <ul> elements for each category. Your JavaScript needs to read the files array and create list items inside the correct <ul> based on each file's category.
- In script.js, add the following renderFiles function below the fileIcons object:
function renderFiles(filteredFiles) {
const categories = ["documents", "videos", "images"];
categories.forEach(function(category) {
const list = document.getElementById(category + "-list");
const categoryFiles = filteredFiles.filter(function(file) {
return file.category === category;
});
// Clear the existing list before re-rendering
list.innerHTML = "";
categoryFiles.forEach(function(file) {
const li = document.createElement("li");
li.className = "file-item";
li.setAttribute("role", "button");
li.setAttribute("tabindex", "0");
// Add the file type icon
const icon = document.createElement("span");
icon.className = "file-item-icon";
icon.textContent = fileIcons[file.type] || "\uD83D\uDCC1";
// Add the file name
const name = document.createElement("span");
name.textContent = file.name;
li.appendChild(icon);
li.appendChild(name);
// Handle tap/click to preview the file
li.addEventListener("click", function() {
selectFile(file, li);
});
// Handle keyboard activation for accessibility
li.addEventListener("keydown", function(e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
selectFile(file, li);
}
});
list.appendChild(li);
});
// Hide categories with no matching files
const section = list.closest(".category");
if (categoryFiles.length === 0) {
section.style.display = "none";
} else {
section.style.display = "";
}
});
}
What does this code do?
This function loops through each category name, finds the matching <ul> element by its ID (like documents-list), and filters the file array to only files in that category.
For each matching file, it creates an <li> element with the correct icon and name, then attaches click and keyboard listeners that will call selectFile (which you will write in substep 3). If a category has zero files, the entire section is hidden.
You won't see a change yet because the function is defined but never called. Next, you will add the category toggle and the initialization calls that bring everything to life.
- Below the renderFiles function, add the initCategoryToggles function and the initialization calls at the very bottom of the file:
function initCategoryToggles() {
var headings = document.querySelectorAll(".category-heading");
headings.forEach(function(heading) {
heading.addEventListener("click", function() {
// Toggle the aria-expanded attribute on tap
var expanded = heading.getAttribute("aria-expanded") === "true";
heading.setAttribute("aria-expanded", String(!expanded));
});
});
}
// Initialize the app
renderFiles(files);
initCategoryToggles();
What does this code do?
The initCategoryToggles function finds every .category-heading button and adds a click listener that flips the aria-expanded attribute between true and false. Your CSS already has a rule that hides .category-items when the sibling heading has aria-expanded="false".
The two calls at the bottom run immediately when the page loads. renderFiles(files) populates the category lists with all eight files. initCategoryToggles() activates the accordion behavior on each heading.
- Save script.js.
- Refresh your browser. You should see file names with icons listed under each category heading (Documents, Videos, Images).
- Tap a category heading (like Videos). The file list beneath it should collapse and disappear. Tap it again to expand.
Don't see any files listed?
- Open the browser console (press F12, then click the Console tab). Look for error messages. A typo in document.getElementById or a mismatched ID like document-list instead of documents-list is a common cause.
- Make sure your renderFiles function is placed after the fileIcons object, and the renderFiles(files); call is at the very bottom of the file.
- Check that your HTML file still has <script src="script.js"></script> just before the closing </body> tag.
Help me debug why my file list isn't rendering in the document hub.
Implement the search functionality
The search bar is already in the HTML, but typing in it does nothing. You need a function that listens for input, filters the files array by name, and re-renders the results.
- In script.js, add the initSearch function above initCategoryToggles:
function initSearch() {
var searchInput = document.getElementById("searchInput");
searchInput.addEventListener("input", function() {
var query = searchInput.value.toLowerCase().trim();
// If the search bar is empty, show all files
if (query === "") {
renderFiles(files);
return;
}
// Filter files whose name contains the search query
var filtered = files.filter(function(file) {
return file.name.toLowerCase().includes(query);
});
renderFiles(filtered);
});
}
What does this code do?
Every time the user types (or deletes) a character, the input event fires. The function grabs the current text, converts it to lowercase for case-insensitive matching, and filters the files array to only names that contain the query.
It then calls renderFiles with the filtered results. Because renderFiles already hides empty categories, non-matching sections disappear automatically.
You also need to call initSearch() when the page loads so the event listener gets attached.
- Scroll to the bottom of script.js and find these two lines:
renderFiles(files);
initCategoryToggles();
- Add initSearch(); between them, so the bottom of your file now looks like this:
renderFiles(files);
initSearch();
initCategoryToggles();
- Save script.js and refresh your browser.
- Type report into the search bar. Only Project Report.pdf should remain visible, and the Videos and Images categories should disappear.
- Clear the search bar. All files and categories should reappear.
Search not filtering results?
- Confirm the input element's ID in your HTML is searchInput (capital I). JavaScript IDs are case-sensitive.
- Make sure you added initSearch(); to the bottom of the file. Without that call, the event listener is never attached.
Help me figure out why my search bar isn't filtering files.
Implement the preview panel
When a user taps a file, the preview panel at the bottom should display that file. Different file types need different HTML elements to render correctly.
- In script.js, add the selectFile function directly below renderFiles:
function selectFile(file, element) {
// Remove the selected style from any previously selected item
document.querySelectorAll(".file-item.selected").forEach(function(item) {
item.classList.remove("selected");
});
// Highlight the tapped item and show its preview
element.classList.add("selected");
showPreview(file);
}
What does this code do?
When a file item is tapped, selectFile first removes the .selected class from any previously highlighted item (so only one item is ever highlighted at a time). It then adds .selected to the tapped element and calls showPreview to update the preview panel.
You won't see a change yet because showPreview does not exist. The next chunk adds it.
- Directly below selectFile, add the showPreview function:
function showPreview(file) {
var panel = document.getElementById("previewPanel");
// Display images with an img tag
if (file.type === "jpg" || file.type === "png") {
panel.innerHTML = '<img src="' + file.path + '" alt="' + file.name + '">';
// Embed PDFs in an iframe
} else if (file.type === "pdf") {
panel.innerHTML = '<iframe src="' + file.path + '" title="' + file.name + '"></iframe>';
// Show videos with the video element
} else if (file.type === "mp4") {
if (file.path) {
panel.innerHTML = '<video controls src="' + file.path + '">Your browser does not support the video tag.</video>';
} else {
panel.innerHTML = '<p class="preview-placeholder">No video file available for preview</p>';
}
// For other file types, show a download link
} else {
panel.innerHTML = '<p class="preview-placeholder">' + file.name + '</p><a href="' + file.path + '" download style="color: var(--accent);">Download file</a>';
}
}
What does this code do?
This function checks the file's type and injects the appropriate HTML into the preview panel:
- Images (jpg/png) get an <img> tag that scales to fit the panel.
- PDFs get an <iframe> so the browser's built-in PDF viewer loads them inline.
- Videos get a <video> element with playback controls. If no file path exists, it shows a placeholder message instead.
- Everything else shows the filename and a download link.
- Save script.js and refresh your browser.
- Tap Team Photo.jpg in the Images category. Your sample image should appear in the preview panel at the bottom-left.
- Tap Training Video.mp4. The preview panel should display a message saying no video file is available (since the sample video path is empty).
- Tap Project Report.pdf. The preview panel should show the contents of your sample text file embedded in an iframe.
Preview panel not updating when you tap a file?
- Open the browser console (F12) and look for errors. If you see selectFile is not defined, make sure the selectFile function is placed between renderFiles and initSearch (it must be defined before it is called).
- Check that previewPanel in your JavaScript matches id="previewPanel" in your HTML. IDs are case-sensitive.
- Verify your image file actually exists at files/sample-image.jpg. If the path is wrong, the image will not display.
Help me debug why tapping a file doesn't update the preview panel.
✔️ Awesome, I've got everything!
Great. Double check you have saved script.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete script.js file at this point. Compare it against yours:
const files = [
{ name: "Project Report.pdf", category: "documents", type: "pdf", path: "files/sample.txt" },
{ name: "Meeting Notes.docx", category: "documents", type: "docx", path: "files/sample.txt" },
{ name: "README.txt", category: "documents", type: "txt", path: "files/sample.txt" },
{ name: "Training Video.mp4", category: "videos", type: "mp4", path: "" },
{ name: "Product Demo.mp4", category: "videos", type: "mp4", path: "" },
{ name: "Team Photo.jpg", category: "images", type: "jpg", path: "files/sample-image.jpg" },
{ name: "Logo.png", category: "images", type: "png", path: "files/sample-image.jpg" },
{ name: "Diagram.png", category: "images", type: "png", path: "files/sample-image.jpg" }
];
const fileIcons = {
pdf: "\uD83D\uDCC4",
docx: "\uD83D\uDCDD",
txt: "\uD83D\uDCC3",
mp4: "\uD83C\uDFAC",
jpg: "\uD83D\uDDBC\uFE0F",
png: "\uD83D\uDDBC\uFE0F"
};
function renderFiles(filteredFiles) {
const categories = ["documents", "videos", "images"];
categories.forEach(function(category) {
const list = document.getElementById(category + "-list");
const categoryFiles = filteredFiles.filter(function(file) {
return file.category === category;
});
list.innerHTML = "";
categoryFiles.forEach(function(file) {
const li = document.createElement("li");
li.className = "file-item";
li.setAttribute("role", "button");
li.setAttribute("tabindex", "0");
const icon = document.createElement("span");
icon.className = "file-item-icon";
icon.textContent = fileIcons[file.type] || "\uD83D\uDCC1";
const name = document.createElement("span");
name.textContent = file.name;
li.appendChild(icon);
li.appendChild(name);
li.addEventListener("click", function() {
selectFile(file, li);
});
li.addEventListener("keydown", function(e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
selectFile(file, li);
}
});
list.appendChild(li);
});
const section = list.closest(".category");
if (categoryFiles.length === 0) {
section.style.display = "none";
} else {
section.style.display = "";
}
});
}
function selectFile(file, element) {
document.querySelectorAll(".file-item.selected").forEach(function(item) {
item.classList.remove("selected");
});
element.classList.add("selected");
showPreview(file);
}
function showPreview(file) {
var panel = document.getElementById("previewPanel");
if (file.type === "jpg" || file.type === "png") {
panel.innerHTML = '<img src="' + file.path + '" alt="' + file.name + '">';
} else if (file.type === "pdf") {
panel.innerHTML = '<iframe src="' + file.path + '" title="' + file.name + '"></iframe>';
} else if (file.type === "mp4") {
if (file.path) {
panel.innerHTML = '<video controls src="' + file.path + '">Your browser does not support the video tag.</video>';
} else {
panel.innerHTML = '<p class="preview-placeholder">No video file available for preview</p>';
}
} else {
panel.innerHTML = '<p class="preview-placeholder">' + file.name + '</p><a href="' + file.path + '" download style="color: var(--accent);">Download file</a>';
}
}
function initSearch() {
var searchInput = document.getElementById("searchInput");
searchInput.addEventListener("input", function() {
var query = searchInput.value.toLowerCase().trim();
if (query === "") {
renderFiles(files);
return;
}
var filtered = files.filter(function(file) {
return file.name.toLowerCase().includes(query);
});
renderFiles(filtered);
});
}
function initCategoryToggles() {
var headings = document.querySelectorAll(".category-heading");
headings.forEach(function(heading) {
heading.addEventListener("click", function() {
var expanded = heading.getAttribute("aria-expanded") === "true";
heading.setAttribute("aria-expanded", String(!expanded));
});
});
}
renderFiles(files);
initSearch();
initCategoryToggles();
Your document hub is now fully interactive. Files render dynamically, search filters in real time, categories collapse and expand, and the preview panel responds to every tap. Next up, you will deploy this site live to the internet with Cloudflare Pages.
Deploy to Cloudflare Pages
Your document hub works perfectly on your machine. Now it is time to put it on the internet so anyone with the link can use it.
Cloudflare Pages lets you deploy static websites for free with unlimited bandwidth. You will upload your entire project folder through the dashboard and get a live URL in seconds.
In this step, get ready to:
- Deploy your site to Cloudflare Pages via drag and drop.
- Test your live site in a browser and confirm all features work.
Deploy your site via drag and drop
Cloudflare Pages supports a drag-and-drop upload method that requires no CLI tools. You will upload your document-hub folder directly through the Cloudflare dashboard.
- Switch back to the Cloudflare dashboard in your browser.
- Click Workers & Pages in the left sidebar.
- Click Create application.
- Click Get started.
- Click Drag and drop your files.
- In the project name field, type my-document-hub.
- Open File Explorer on your Windows PC and navigate to your Desktop.
- Drag your entire document-hub folder from File Explorer into the upload area on the Cloudflare page.
- Wait for all files to finish uploading (you will see green checkmarks next to each file).
- Click Deploy site.
What just happened?
Cloudflare uploaded your HTML, CSS, JavaScript, and sample files to its global network. Your site is now served from data centers around the world, making it fast for anyone to access.
The URL follows the pattern <project-name>.pages.dev. Since you named your project my-document-hub, your site lives at my-document-hub.pages.dev.
- Confirm you see a success message with your live URL displayed on the page.
Seeing a 404 error when you visit your URL?
Your index.html file must be at the top level of the uploaded folder. If you accidentally dragged a parent folder that wraps document-hub, the files end up nested one level too deep.
Delete the project in Workers & Pages and re-upload, making sure you drag the document-hub folder itself (the one that directly contains index.html).
Help me fix my Cloudflare Pages 404 error.
Test your live site
A deployment is only useful if the site actually works. You need to verify that all the interactive features function correctly at the live URL.
- Copy the .pages.dev URL from the Cloudflare success page.
- Open a new browser tab and paste the URL.
- Confirm the dark-themed layout loads with the search bar at the top, categories in the middle, and the preview panel at the bottom.
- Type a file name (like Report) in the search bar and confirm files filter in real time.
- Clear the search and tap a category heading to confirm it collapses and expands.
- Tap a file item (like an image) and confirm the preview panel updates to show that file.
- If you have a phone available, open the same URL on your phone and confirm the touch targets are easy to tap.
Why test on a phone?
This project is designed as a touch-first interface. Testing on an actual touch device confirms that the 48px minimum-height buttons, the scale feedback on tap, and the responsive stacking all work as intended.
- Confirm that search, category toggling, and file preview all work identically to your local version.
Site loads but looks broken or unstyled?
This usually means style.css or script.js failed to load. Open your browser's DevTools (press F12) and check the Console tab for 404 errors on those files.
Make sure your index.html references style.css and script.js (not a subfolder path like css/style.css). Re-upload if needed.
Help me debug why my deployed site looks unstyled.
Your document hub is live on the internet. Anyone with the link can browse, search, and preview your files from any device in the world.
Secret mission
Add a Light Mode Toggle
Your document hub looks great in dark mode. But what if someone prefers a lighter interface? In this bonus challenge, you will add a toggle button that switches the entire UI from dark to light with smooth CSS transitions and remembered preferences.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on the Cloudflare Pages free tier, 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:
- One Cloudflare Pages project (my-document-hub, free tier).
- Local document-hub folder on your Desktop (with index.html, style.css, script.js, and files/).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep your site live.
- Your site stays live at your .pages.dev URL indefinitely at no cost.
- Cloudflare Pages free tier includes unlimited bandwidth and 500 builds per month.
- To update the site later, drag and drop a new version of your folder to create a new deployment.
Pause - I'll come back to this later
Since Cloudflare Pages is a static hosting service with no running processes, there is nothing to pause. Your site stays live at no cost. Focus on keeping your local files safe so you can redeploy later.
- Keep your document-hub folder on your Desktop so you can redeploy or edit later.
- Close any browser tabs open to the Cloudflare dashboard.
- When you are ready to come back, log into Cloudflare and drag-and-drop your updated folder to redeploy.
Delete - I don't want to use this again
Remove all project resources and start fresh. This takes your site offline permanently.
- Delete your Cloudflare Pages project:
- Delete your local project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf document-hub
- On Windows, run:
cd ~\Desktop
Remove-Item -Recurse -Force document-hub
After these steps, your site is offline and all local files are removed.
Nice Work!
Nice Work!
You just built and deployed a fully functional, touch-friendly document hub from scratch. Your site is live on the internet and accessible from any device in the world.
You've learned how to:
- Build a CSS Grid layout with a modern dark theme and touch-optimized 48px+ tap targets designed for fingers on glass.
- Implement real-time search filtering and category toggling with vanilla JavaScript, plus an inline file preview system supporting images, PDFs, and videos.
- Deploy a live website to Cloudflare Pages using drag-and-drop direct upload, making it accessible from anywhere in the world.
- Secret Mission: Added a light mode toggle with smooth CSS transitions and localStorage persistence across page reloads.
Ready to quiz yourself?