Overview
Create full-page interfaces for your extension using custom HTML pages. This lesson builds a notes management page listing all saved notes, demonstrating how to create standalone extension pages beyond popups.
By the end, TabNotes will have a dedicated page showing all notes with links to their corresponding websites.
What You’ll Learn
- Create custom extension pages in WXT
- Build unlisted pages (accessed via URL, not browser UI)
- Access extension storage from custom pages
- Display and format stored data
- Link to extension pages from popups
- Handle empty states and error conditions
Extension Page Types
Extensions support several page types beyond popups:
| Page Type | Manifest Key | Use Case |
|---|---|---|
| Popup | action.default_popup | Quick actions, minimal UI |
| Options | options_page | Extension settings |
| DevTools | devtools_page | Browser DevTools panels |
| New Tab | chrome_url_overrides.newtab | Replace new tab page |
| Unlisted | None | Custom pages accessed by URL |
For TabNotes: Create an unlisted page for viewing all notes.
Creating Unlisted Pages in WXT
Unlisted pages don’t appear in browser UI - users access them via direct URL.
Step 1: Create the Page File
entrypoints/
└── notes-list.html
WXT automatically makes this page accessible at:
chrome-extension://[extension-id]/notes-list.html
Step 2: Build the HTML Structure
<!-- entrypoints/notes-list.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>All Notes - TabNotes</title>
</head>
<body>
<div id="app">
<h1>All Your Notes</h1>
<div id="notes-container"></div>
</div>
<script type="module" src="./notes-list.ts"></script>
</body>
</html>
Step 3: Create the Script File
// entrypoints/notes-list.ts
import './notes-list.css';
import { storage } from 'wxt/storage';
document.addEventListener('DOMContentLoaded', async () => {
const container = document.getElementById('notes-container');
// Load all notes
const notesStorage = await storage.getItem('sync:notesStorage') || {};
// Render notes
if (Object.keys(notesStorage).length === 0) {
container.innerHTML = '<p>No notes yet. Start browsing and take notes!</p>';
} else {
container.innerHTML = Object.entries(notesStorage)
.map(([pageKey, noteData]) => `
<div class="note-card">
<h3>${pageKey}</h3>
<p>${noteData.note}</p>
<small>Updated: ${new Date(noteData.updatedAt).toLocaleDateString()}</small>
</div>
`)
.join('');
}
});
Styling the Notes List
/* entrypoints/notes-list.css */
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
#app {
max-width: 800px;
margin: 0 auto;
}
h1 {
color: #333;
margin-bottom: 30px;
}
.note-card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.note-card h3 {
margin: 0 0 10px 0;
color: #444;
font-size: 16px;
}
.note-card p {
margin: 0 0 10px 0;
color: #666;
line-height: 1.5;
}
.note-card small {
color: #999;
font-size: 12px;
}
Linking to Custom Pages
From Popup
Add a button in the popup to open the notes list:
// entrypoints/popup/main.ts
const viewAllButton = document.createElement('button');
viewAllButton.textContent = 'View All Notes';
viewAllButton.addEventListener('click', () => {
browser.tabs.create({
url: browser.runtime.getURL('notes-list.html')
});
});
document.querySelector('#app').appendChild(viewAllButton);
browser.runtime.getURL() - Converts relative path to full extension URL.
Opening in Current vs New Tab
// Open in new tab
browser.tabs.create({ url: extensionURL });
// Open in current tab
browser.tabs.update({ url: extensionURL });
Displaying Note Metadata
Enhance notes display with more information:
// entrypoints/notes-list.ts
const formatNote = (pageKey: string, noteData: any) => {
const createdDate = new Date(noteData.createdAt).toLocaleString();
const updatedDate = new Date(noteData.updatedAt).toLocaleString();
const wordCount = noteData.note.split(/\s+/).length;
return `
<div class="note-card">
<div class="note-header">
<h3>${pageKey}</h3>
<a href="https://${pageKey}" target="_blank">Visit Page</a>
</div>
<p class="note-content">${noteData.note}</p>
<div class="note-meta">
<span>${wordCount} words</span>
<span>Created: ${createdDate}</span>
<span>Updated: ${updatedDate}</span>
</div>
</div>
`;
};
Adding Delete Functionality
Allow users to delete notes from the list:
// entrypoints/notes-list.ts
const deleteNote = async (pageKey: string) => {
const notesStorage = await storage.getItem('sync:notesStorage') || {};
delete notesStorage[pageKey];
await storage.setItem('sync:notesStorage', notesStorage);
// Reload page to reflect changes
window.location.reload();
};
// In render function
const formatNote = (pageKey: string, noteData: any) => {
return `
<div class="note-card">
<h3>${pageKey}</h3>
<p>${noteData.note}</p>
<button class="delete-btn" data-page-key="${pageKey}">Delete</button>
</div>
`;
};
// Add event listeners after rendering
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const pageKey = (e.target as HTMLElement).dataset.pageKey;
if (confirm('Delete this note?')) {
deleteNote(pageKey);
}
});
});
Search and Filter
Add search functionality:
// entrypoints/notes-list.ts
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search notes...';
searchInput.addEventListener('input', (e) => {
const query = (e.target as HTMLInputElement).value.toLowerCase();
filterNotes(query);
});
const filterNotes = (query: string) => {
const cards = document.querySelectorAll('.note-card');
cards.forEach(card => {
const text = card.textContent?.toLowerCase() || '';
card.style.display = text.includes(query) ? 'block' : 'none';
});
};
Options Pages vs Unlisted Pages
Options Pages
Accessible via chrome://extensions → Extension Details → “Extension options”:
// entrypoints/options.html
// Automatically linked in manifest as options_page
Use For: Extension settings, preferences
Unlisted Pages
No automatic browser UI:
// entrypoints/custom-page.html
// Accessible only via direct URL
Use For: Feature pages, dashboards, data views
TabNotes uses unlisted page - notes list is a feature, not settings.
Debugging Custom Pages
Opening DevTools
- Navigate to your custom page
- Right-click → Inspect
- DevTools opens like normal webpage
Common Issues
Page Not Found (404):
- Check file exists in
entrypoints/ - Verify URL:
browser.runtime.getURL('filename.html') - Restart dev server
Storage Empty:
// Add debug logging
const notesStorage = await storage.getItem('sync:notesStorage');
console.log('Loaded storage:', notesStorage);
What’s Next
TabNotes now has a dedicated notes management page where users can view all saved notes in one place. The extension is feature-complete from a functionality standpoint.
Lesson 12 covers extension lifecycle and service worker best practices - understanding how background scripts start, stop, and handle state management.