Overview
Apply final polish to TabNotes before store submission - fixing UX issues, improving accessibility, and adding quality-of-life features. This lesson covers the small details that separate amateur extensions from professional ones.
By the end, TabNotes will be store-ready with polished interactions and professional user experience.
What You’ll Learn
- Auto-focus textarea for better UX
- Handle empty states gracefully
- Add keyboard shortcuts
- Improve error messages
- Polish visual details
- Test edge cases
- Prepare for production
Auto-Focus Textarea
Problem: Users must click textarea before typing.
Fix: Auto-focus on popup open:
// entrypoints/popup/main.ts
const loadExistingNote = async () => {
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true
});
if (!tab || !tab.url) return;
const pageKey = getPageKey(tab.url);
const notesStorage = await storage.getItem('sync:notesStorage');
if (notesStorage?.[pageKey]) {
noteText.value = notesStorage[pageKey].note;
}
// Auto-focus for immediate typing
noteText.focus();
};
loadExistingNote();
Test: Open popup → Cursor blinks in textarea immediately
Empty State Handling
Popup Empty State
Show placeholder when no notes exist:
const loadExistingNote = async () => {
// ... existing code ...
if (notesStorage?.[pageKey]) {
noteText.value = notesStorage[pageKey].note;
} else {
noteText.placeholder = 'Start typing your note...';
}
noteText.focus();
};
Notes List Empty State
Improve messaging when no notes exist:
// entrypoints/notes-list.ts
if (Object.keys(notesStorage).length === 0) {
container.innerHTML = `
<div class="empty-state">
<h2>No notes yet</h2>
<p>Visit any website and click the extension icon to start taking notes.</p>
</div>
`;
}
Keyboard Shortcuts
Add keyboard shortcuts for common actions:
Close Popup on Escape
// entrypoints/popup/main.ts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
window.close();
}
});
Save on Ctrl+S
noteText.addEventListener('keydown', async (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault(); // Prevent browser save dialog
// Trigger save (already happens on keyup, but provide feedback)
const saveIndicator = document.createElement('div');
saveIndicator.textContent = 'Saved!';
saveIndicator.className = 'save-indicator';
document.body.appendChild(saveIndicator);
setTimeout(() => saveIndicator.remove(), 1000);
}
});
Error Handling
Graceful Storage Failures
const saveNote = async (noteData: any) => {
try {
await storage.setItem('sync:notesStorage', notesStorage);
} catch (error) {
console.error('Failed to save:', error);
// Show user-friendly error
const errorMsg = document.createElement('div');
errorMsg.className = 'error-message';
errorMsg.textContent = 'Failed to save note. Please try again.';
document.body.appendChild(errorMsg);
setTimeout(() => errorMsg.remove(), 3000);
}
};
Handle Tab Access Errors
const getCurrentTab = async () => {
try {
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true
});
if (!tab || !tab.url) {
throw new Error('Cannot access current tab');
}
return tab;
} catch (error) {
console.error('Tab access error:', error);
// Show helpful message
noteText.disabled = true;
noteText.placeholder = 'Cannot save notes on this page';
return null;
}
};
Visual Polish
Loading States
Show feedback during async operations:
// entrypoints/notes-list.ts
const container = document.getElementById('notes-container');
container.innerHTML = '<div class="loading">Loading notes...</div>';
const notesStorage = await storage.getItem('sync:notesStorage') || {};
// Clear loading, show notes
container.innerHTML = renderNotes(notesStorage);
Smooth Animations
/* entrypoints/popup/style.css */
textarea {
transition: opacity 0.2s;
}
textarea:focus {
opacity: 1;
}
.save-indicator {
position: fixed;
top: 10px;
right: 10px;
background: #4caf50;
color: white;
padding: 8px 16px;
border-radius: 4px;
animation: slideIn 0.3s;
}
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
Accessibility Improvements
ARIA Labels
<textarea
id="note-text"
aria-label="Note content"
aria-describedby="note-help"
></textarea>
<span id="note-help" class="sr-only">
Type your note here. It saves automatically.
</span>
Keyboard Navigation
Ensure all interactive elements are keyboard-accessible:
button:focus,
textarea:focus {
outline: 2px solid #4caf50;
outline-offset: 2px;
}
Testing Edge Cases
Test Scenarios
Special Characters:
- Type notes with emoji, unicode, special chars
- Verify proper storage and display
Very Long Notes:
- Type 10,000+ character note
- Check performance, storage limits
Rapid Typing:
- Type very quickly
- Ensure no lost characters
Multiple Tabs:
- Open same site in multiple tabs
- Verify consistent note display
Private/Incognito Mode:
- Test extension in incognito
- Ensure expected behavior (or disable in incognito)
Network Conditions
Test with sync storage:
- Sign out of Chrome → Notes stay local
- Sign back in → Notes sync
Performance Optimizations
Debounce Auto-Save
Reduce storage writes during typing:
let saveTimeout: number;
noteText.addEventListener('keyup', () => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
await saveNote();
await updateIcon();
await sendMessage();
}, 300); // Wait 300ms after last keystroke
});
Lazy Load Notes List
For users with many notes:
// entrypoints/notes-list.ts
const renderNotes = (notesStorage: any, limit = 50) => {
const entries = Object.entries(notesStorage);
const visible = entries.slice(0, limit);
return visible.map(([key, note]) => renderNote(key, note)).join('');
};
// Add "Load More" button if needed
if (Object.keys(notesStorage).length > 50) {
// Show load more button
}
Final Checklist
Before submitting to store:
- Extension name, description, version set in
wxt.config.ts - All icons created (16, 32, 48, 96, 128px)
- No console errors or warnings
- All features work in incognito (if enabled)
- Tested on Chrome and Firefox
- Empty states handled gracefully
- Error messages are user-friendly
- Loading states provide feedback
- Keyboard shortcuts documented
- Permissions minimized
- README includes usage instructions
Production Build
Building for Production
bun run build # or npm run build
bun run zip # or npm run zip
Creates optimized bundles in .output/:
.output/chrome-mv3/- Chrome production build.output/firefox-mv3/- Firefox production build.output/chrome-mv3.zip- Ready for Chrome Web Store.output/firefox-mv3.zip- Ready for Firefox Add-ons
Verifying Build
- Load
.output/chrome-mv3/in Chrome - Test all features work
- Check file sizes (should be smaller than dev build)
- Verify no dev tools or debug code included
What’s Next
TabNotes is polished and production-ready. All features work smoothly, edge cases are handled, and the user experience is professional.
Lesson 14 covers the submission process - uploading to Chrome Web Store and Firefox Add-ons, writing store listings, handling reviews, and publishing your extension to millions of users.