Cara Buat Chrome Extension dengan React dan Vite
Senin, 29 Des 2025
Pernah kepikiran bikin extension Chrome sendiri? Mungkin ada fitur yang kamu butuhkan tapi belum ada extensionnya, atau kamu ingin automate task tertentu di browser.
Kabar baiknya, dengan React dan Vite, proses development Chrome Extension jadi jauh lebih enjoyable. Kamu bisa pakai semua fitur React yang sudah familiar: components, hooks, state management—semuanya bisa dipakai.
Di tutorial ini, kita akan bikin extension productivity sederhana: Quick Notes. Extension yang memungkinkan user menyimpan catatan cepat dan highlight text di halaman manapun.
Apa Itu Chrome Extension?
Chrome Extension adalah program kecil yang memodifikasi atau enhance pengalaman browsing di Chrome. Extension bisa:
- Menambah fitur baru ke browser (ad blocker, password manager)
- Memodifikasi tampilan website (dark mode, font changer)
- Integrate dengan services lain (Grammarly, Notion)
- Automate tasks (form filler, screenshot tool)
Extension yang populer seperti uBlock Origin, React DevTools, dan Grammarly—semuanya adalah Chrome Extension.
Anatomi Chrome Extension
Sebelum coding, penting untuk paham struktur dasar Chrome Extension:
my-extension/
├── manifest.json # File konfigurasi utama
├── popup/ # UI yang muncul saat klik icon extension
│ ├── index.html
│ └── App.tsx
├── content-scripts/ # Script yang berjalan di halaman web
│ └── content.ts
├── background/ # Service worker (background process)
│ └── background.ts
├── options/ # Halaman settings extension
│ └── Options.tsx
└── public/
└── icons/ # Icon extension berbagai ukuran
1. manifest.json
File paling penting. Berisi metadata dan permission yang dibutuhkan extension.
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"description": "Deskripsi extension",
"permissions": ["storage", "activeTab"],
"action": {
"default_popup": "popup.html",
"default_icon": "icon-48.png"
}
}
2. Popup
UI yang muncul ketika user klik icon extension di toolbar. Ini adalah “frontend” utama extension kamu—dan di sinilah React bersinar.
3. Content Scripts
JavaScript yang di-inject ke halaman web. Content scripts bisa membaca dan memodifikasi DOM halaman yang sedang dibuka user.
4. Background Script (Service Worker)
Script yang berjalan di background, terpisah dari halaman web. Digunakan untuk event handling, API calls, atau operasi yang perlu persist.
5. Options Page
Halaman settings untuk extension. User bisa akses via right-click icon extension → Options.
Manifest V3 vs V2
Sejak 2023, Chrome mewajibkan Manifest V3 untuk extension baru. Perbedaan utamanya:
| Aspek | Manifest V2 | Manifest V3 |
|---|---|---|
| Background | Persistent background pages | Service workers (event-driven) |
| API | chrome.browserAction | chrome.action |
| Content Security | Lebih loose | Lebih strict |
| Remote Code | Diizinkan | Tidak diizinkan |
| Permissions | Granted sekali | Host permissions bisa di-revoke |
Kita akan pakai Manifest V3 di tutorial ini.
Setup Project dengan Vite
Ada beberapa template untuk Chrome Extension dengan React, tapi kita akan pakai CRXJS—Vite plugin yang bikin development extension jadi seamless.
1. Create Project
npm create vite@latest quick-notes-extension -- --template react-ts
cd quick-notes-extension
2. Install CRXJS Plugin
npm install @crxjs/vite-plugin -D
3. Setup Vite Config
// filepath: /vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'
export default defineConfig({
plugins: [
react(),
crx({ manifest }),
],
})
4. Buat manifest.json
{
"manifest_version": 3,
"name": "Quick Notes",
"version": "1.0.0",
"description": "Simpan catatan cepat dan highlight text di halaman manapun",
"permissions": [
"storage",
"activeTab",
"scripting"
],
"action": {
"default_popup": "index.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content/content.ts"],
"css": ["src/content/content.css"]
}
],
"background": {
"service_worker": "src/background/background.ts",
"type": "module"
}
}
5. Struktur Folder
Reorganize project structure:
quick-notes-extension/
├── public/
│ └── icons/
│ ├── icon-16.png
│ ├── icon-48.png
│ └── icon-128.png
├── src/
│ ├── popup/
│ │ ├── App.tsx
│ │ ├── App.css
│ │ └── components/
│ ├── content/
│ │ ├── content.ts
│ │ └── content.css
│ ├── background/
│ │ └── background.ts
│ ├── utils/
│ │ └── storage.ts
│ └── types/
│ └── index.ts
├── index.html
├── manifest.json
├── vite.config.ts
└── package.json
6. Update index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quick Notes</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/popup/main.tsx"></script>
</body>
</html>
7. Create Entry Point
// filepath: /src/popup/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './App.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
Building Popup UI dengan React
Sekarang mari bikin UI untuk popup. Kita akan buat interface untuk menyimpan dan menampilkan notes.
Types Definition
// filepath: /src/types/index.ts
export interface Note {
id: string;
content: string;
url: string;
title: string;
createdAt: number;
color?: string;
}
export interface Highlight {
id: string;
text: string;
url: string;
createdAt: number;
}
Storage Utility
Chrome Extension punya built-in storage API. Mari buat wrapper untuk memudahkan:
// filepath: /src/utils/storage.ts
import { Note, Highlight } from '../types';
const NOTES_KEY = 'quick_notes';
const HIGHLIGHTS_KEY = 'quick_highlights';
export const storage = {
// Notes
async getNotes(): Promise<Note[]> {
const result = await chrome.storage.local.get(NOTES_KEY);
return result[NOTES_KEY] || [];
},
async saveNote(note: Note): Promise<void> {
const notes = await this.getNotes();
notes.unshift(note);
await chrome.storage.local.set({ [NOTES_KEY]: notes });
},
async deleteNote(id: string): Promise<void> {
const notes = await this.getNotes();
const filtered = notes.filter(n => n.id !== id);
await chrome.storage.local.set({ [NOTES_KEY]: filtered });
},
// Highlights
async getHighlights(): Promise<Highlight[]> {
const result = await chrome.storage.local.get(HIGHLIGHTS_KEY);
return result[HIGHLIGHTS_KEY] || [];
},
async saveHighlight(highlight: Highlight): Promise<void> {
const highlights = await this.getHighlights();
highlights.unshift(highlight);
await chrome.storage.local.set({ [HIGHLIGHTS_KEY]: highlights });
},
async deleteHighlight(id: string): Promise<void> {
const highlights = await this.getHighlights();
const filtered = highlights.filter(h => h.id !== id);
await chrome.storage.local.set({ [HIGHLIGHTS_KEY]: filtered });
},
// Clear all
async clearAll(): Promise<void> {
await chrome.storage.local.clear();
}
};
Popup App Component
// filepath: /src/popup/App.tsx
import { useState, useEffect } from 'react';
import { Note } from '../types';
import { storage } from '../utils/storage';
import NoteList from './components/NoteList';
import NoteForm from './components/NoteForm';
import './App.css';
function App() {
const [notes, setNotes] = useState<Note[]>([]);
const [activeTab, setActiveTab] = useState<'notes' | 'highlights'>('notes');
const [currentUrl, setCurrentUrl] = useState('');
const [currentTitle, setCurrentTitle] = useState('');
useEffect(() => {
loadNotes();
getCurrentTab();
}, []);
const loadNotes = async () => {
const storedNotes = await storage.getNotes();
setNotes(storedNotes);
};
const getCurrentTab = async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) {
setCurrentUrl(tab.url || '');
setCurrentTitle(tab.title || '');
}
};
const handleAddNote = async (content: string) => {
const newNote: Note = {
id: crypto.randomUUID(),
content,
url: currentUrl,
title: currentTitle,
createdAt: Date.now(),
};
await storage.saveNote(newNote);
setNotes(prev => [newNote, ...prev]);
};
const handleDeleteNote = async (id: string) => {
await storage.deleteNote(id);
setNotes(prev => prev.filter(n => n.id !== id));
};
return (
<div className="popup-container">
<header className="popup-header">
<h1>📝 Quick Notes</h1>
<p className="current-page" title={currentTitle}>
{currentTitle.slice(0, 30) || 'New Tab'}
{currentTitle.length > 30 && '...'}
</p>
</header>
<nav className="tabs">
<button
className={activeTab === 'notes' ? 'active' : ''}
onClick={() => setActiveTab('notes')}
>
Notes ({notes.length})
</button>
<button
className={activeTab === 'highlights' ? 'active' : ''}
onClick={() => setActiveTab('highlights')}
>
Highlights
</button>
</nav>
<main className="popup-content">
{activeTab === 'notes' && (
<>
<NoteForm onSubmit={handleAddNote} />
<NoteList notes={notes} onDelete={handleDeleteNote} />
</>
)}
</main>
</div>
);
}
export default App;
Note Form Component
// filepath: /src/popup/components/NoteForm.tsx
import { useState, FormEvent } from 'react';
interface NoteFormProps {
onSubmit: (content: string) => void;
}
function NoteForm({ onSubmit }: NoteFormProps) {
const [content, setContent] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (content.trim()) {
onSubmit(content.trim());
setContent('');
}
};
return (
<form onSubmit={handleSubmit} className="note-form">
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Tulis catatan..."
rows={3}
/>
<button type="submit" disabled={!content.trim()}>
+ Tambah Note
</button>
</form>
);
}
export default NoteForm;
Note List Component
// filepath: /src/popup/components/NoteList.tsx
import { Note } from '../../types';
interface NoteListProps {
notes: Note[];
onDelete: (id: string) => void;
}
function NoteList({ notes, onDelete }: NoteListProps) {
const formatDate = (timestamp: number) => {
return new Date(timestamp).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
});
};
if (notes.length === 0) {
return (
<div className="empty-state">
<p>Belum ada catatan</p>
<small>Tulis sesuatu di atas 👆</small>
</div>
);
}
return (
<ul className="note-list">
{notes.map((note) => (
<li key={note.id} className="note-item">
<div className="note-content">
<p>{note.content}</p>
<div className="note-meta">
<span className="note-page" title={note.title}>
📄 {note.title.slice(0, 20)}...
</span>
<span className="note-date">{formatDate(note.createdAt)}</span>
</div>
</div>
<button
className="delete-btn"
onClick={() => onDelete(note.id)}
title="Hapus"
>
×
</button>
</li>
))}
</ul>
);
}
export default NoteList;
Styling
/* filepath: /src/popup/App.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 360px;
min-height: 400px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eaeaea;
}
.popup-container {
display: flex;
flex-direction: column;
height: 100%;
}
.popup-header {
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.popup-header h1 {
font-size: 18px;
font-weight: 600;
}
.current-page {
font-size: 12px;
opacity: 0.8;
margin-top: 4px;
}
.tabs {
display: flex;
border-bottom: 1px solid #2d2d44;
}
.tabs button {
flex: 1;
padding: 12px;
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.tabs button.active {
color: #667eea;
border-bottom: 2px solid #667eea;
}
.tabs button:hover {
color: #eaeaea;
}
.popup-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.note-form {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.note-form textarea {
width: 100%;
padding: 12px;
border: 1px solid #2d2d44;
border-radius: 8px;
background: #16162a;
color: #eaeaea;
font-size: 14px;
resize: none;
}
.note-form textarea:focus {
outline: none;
border-color: #667eea;
}
.note-form button {
padding: 10px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: white;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.note-form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.note-form button:hover:not(:disabled) {
opacity: 0.9;
}
.note-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.note-item {
display: flex;
gap: 8px;
padding: 12px;
background: #16162a;
border-radius: 8px;
border: 1px solid #2d2d44;
}
.note-content {
flex: 1;
min-width: 0;
}
.note-content p {
font-size: 14px;
line-height: 1.5;
margin-bottom: 8px;
word-wrap: break-word;
}
.note-meta {
display: flex;
gap: 12px;
font-size: 11px;
color: #666;
}
.delete-btn {
width: 24px;
height: 24px;
background: #2d2d44;
border: none;
border-radius: 4px;
color: #888;
cursor: pointer;
font-size: 16px;
line-height: 1;
}
.delete-btn:hover {
background: #ff4757;
color: white;
}
.empty-state {
text-align: center;
padding: 32px;
color: #666;
}
.empty-state small {
display: block;
margin-top: 8px;
}
Content Scripts: Memodifikasi Halaman Web
Content scripts memungkinkan extension kita berinteraksi dengan halaman yang sedang dibuka user. Kita akan implementasi fitur highlight text.
Content Script
// filepath: /src/content/content.ts
// Listen for text selection
document.addEventListener('mouseup', handleTextSelection);
function handleTextSelection() {
const selection = window.getSelection();
const selectedText = selection?.toString().trim();
if (selectedText && selectedText.length > 0) {
showHighlightButton(selection!);
} else {
removeHighlightButton();
}
}
function showHighlightButton(selection: Selection) {
removeHighlightButton();
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const button = document.createElement('button');
button.id = 'quick-notes-highlight-btn';
button.innerHTML = '✨ Highlight';
button.style.cssText = `
position: fixed;
top: ${rect.top - 40 + window.scrollY}px;
left: ${rect.left + rect.width / 2}px;
transform: translateX(-50%);
z-index: 999999;
padding: 8px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
`;
button.addEventListener('click', async () => {
const text = selection.toString().trim();
await highlightAndSave(text, selection);
removeHighlightButton();
});
document.body.appendChild(button);
}
function removeHighlightButton() {
const existingBtn = document.getElementById('quick-notes-highlight-btn');
if (existingBtn) {
existingBtn.remove();
}
}
async function highlightAndSave(text: string, selection: Selection) {
// Apply highlight style
const range = selection.getRangeAt(0);
const span = document.createElement('span');
span.className = 'quick-notes-highlight';
span.style.cssText = `
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3), rgba(118, 75, 162, 0.3));
padding: 2px 4px;
border-radius: 3px;
`;
range.surroundContents(span);
// Save to storage via background script
chrome.runtime.sendMessage({
type: 'SAVE_HIGHLIGHT',
payload: {
id: crypto.randomUUID(),
text,
url: window.location.href,
createdAt: Date.now(),
}
});
selection.removeAllRanges();
}
// Remove button when clicking elsewhere
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.id !== 'quick-notes-highlight-btn') {
removeHighlightButton();
}
});
Content Script CSS
/* filepath: /src/content/content.css */
.quick-notes-highlight {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3), rgba(118, 75, 162, 0.3)) !important;
padding: 2px 4px !important;
border-radius: 3px !important;
transition: background 0.2s !important;
}
.quick-notes-highlight:hover {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.5), rgba(118, 75, 162, 0.5)) !important;
}
Background Script (Service Worker)
Background script handle events dan komunikasi antar bagian extension.
// filepath: /src/background/background.ts
// Listen for messages from content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'SAVE_HIGHLIGHT') {
saveHighlight(message.payload);
sendResponse({ success: true });
}
return true;
});
async function saveHighlight(highlight: {
id: string;
text: string;
url: string;
createdAt: number;
}) {
const result = await chrome.storage.local.get('quick_highlights');
const highlights = result.quick_highlights || [];
highlights.unshift(highlight);
await chrome.storage.local.set({ quick_highlights: highlights });
}
// Optional: Set up badge to show note count
chrome.storage.onChanged.addListener((changes) => {
if (changes.quick_notes) {
const count = changes.quick_notes.newValue?.length || 0;
chrome.action.setBadge({ text: count > 0 ? String(count) : '' });
chrome.action.setBadgeBackgroundColor({ color: '#667eea' });
}
});
// Handle installation
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('Quick Notes installed!');
}
});
Message Passing: Komunikasi Antar Komponen
Di Chrome Extension, ada beberapa cara komunikasi:
1. Content Script ↔ Background
// Dari content script
chrome.runtime.sendMessage({ type: 'ACTION', data: {} }, (response) => {
console.log('Response:', response);
});
// Di background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'ACTION') {
// Process...
sendResponse({ success: true });
}
return true; // Keep channel open for async response
});
2. Popup ↔ Content Script
// Dari popup, kirim ke content script di tab aktif
async function sendToContentScript(message: any) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab.id) {
return chrome.tabs.sendMessage(tab.id, message);
}
}
// Di content script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GET_SELECTION') {
const selection = window.getSelection()?.toString();
sendResponse({ text: selection });
}
return true;
});
3. Long-lived Connections
Untuk komunikasi yang lebih kompleks:
// Connect
const port = chrome.runtime.connect({ name: 'quick-notes' });
// Send message
port.postMessage({ type: 'PING' });
// Listen for messages
port.onMessage.addListener((msg) => {
console.log('Received:', msg);
});
Chrome Storage API
Chrome Extension punya dua jenis storage:
Local Storage
// Save
await chrome.storage.local.set({ key: 'value' });
// Get
const result = await chrome.storage.local.get('key');
console.log(result.key);
// Get multiple
const { key1, key2 } = await chrome.storage.local.get(['key1', 'key2']);
// Remove
await chrome.storage.local.remove('key');
// Clear all
await chrome.storage.local.clear();
Sync Storage
Sync ke akun Google user (max 100KB total):
// Data akan sync ke semua Chrome yang login akun sama
await chrome.storage.sync.set({ settings: { theme: 'dark' } });
const result = await chrome.storage.sync.get('settings');
Listen for Changes
chrome.storage.onChanged.addListener((changes, areaName) => {
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(`${key} changed from`, oldValue, 'to', newValue);
}
});
Development & Testing
Run Development Server
npm run dev
CRXJS akan generate folder dist/ dengan extension yang siap di-load.
Load Extension ke Chrome
- Buka
chrome://extensions/ - Enable “Developer mode” (toggle di kanan atas)
- Klik “Load unpacked”
- Pilih folder
dist/dari project kamu
Hot Reload
CRXJS support HMR! Perubahan di popup akan langsung ter-update. Untuk content scripts, kamu perlu reload extension.
Debugging
- Popup: Right-click icon extension → Inspect popup
- Background: Di
chrome://extensions/, klik “Service Worker” link - Content Scripts: DevTools di halaman web → Sources → Content Scripts
Build untuk Production
npm run build
Folder dist/ siap untuk di-publish.
Publish ke Chrome Web Store
1. Persiapan
Sebelum publish, siapkan:
- Screenshot extension (1280x800 atau 640x400)
- Promotional images (opsional tapi recommended)
- Deskripsi lengkap
- Privacy policy (jika collect user data)
2. Daftar Developer Account
- Buka Chrome Web Store Developer Dashboard
- Bayar one-time fee $5
- Verify email
3. Package Extension
Zip folder dist/:
cd dist
zip -r ../quick-notes-extension.zip .
4. Upload & Submit
- Di Developer Dashboard, klik “New Item”
- Upload ZIP file
- Isi semua informasi yang diminta
- Submit for review
Review biasanya 1-3 hari kerja. Kalau ada masalah, Chrome akan kirim feedback.
Tips & Best Practices
Performance
// ❌ Bad: Check storage di setiap event
document.addEventListener('scroll', async () => {
const data = await chrome.storage.local.get('settings'); // Expensive!
});
// ✅ Good: Cache data
let cachedSettings: Settings | null = null;
async function getSettings() {
if (!cachedSettings) {
const result = await chrome.storage.local.get('settings');
cachedSettings = result.settings;
}
return cachedSettings;
}
Permissions
Request permission yang benar-benar dibutuhkan:
{
"permissions": ["storage"], // Required
"optional_permissions": ["tabs", "history"], // Request when needed
"host_permissions": ["https://specific-site.com/*"] // Minimal scope
}
Error Handling
try {
await chrome.storage.local.set({ data: value });
} catch (error) {
if (error.message.includes('QUOTA_BYTES')) {
console.error('Storage quota exceeded');
// Handle cleanup
}
}
Content Script Isolation
Content scripts jalan di isolated world, tapi share DOM:
// Access page's window object
const pageWindow = window.wrappedJSObject; // Firefox
// Di Chrome, perlu inject script ke page context
function injectScript(code: string) {
const script = document.createElement('script');
script.textContent = code;
document.head.appendChild(script);
script.remove();
}
Real Example: Productivity Timer
Mari tambahkan fitur pomodoro timer ke extension kita:
// filepath: /src/popup/components/Timer.tsx
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(25 * 60);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
let interval: number;
if (isRunning && seconds > 0) {
interval = setInterval(() => {
setSeconds(prev => prev - 1);
}, 1000);
} else if (seconds === 0) {
// Notification when done
chrome.notifications.create({
type: 'basic',
iconUrl: '/icons/icon-128.png',
title: 'Pomodoro Selesai!',
message: 'Waktunya istirahat 5 menit 🎉',
});
setIsRunning(false);
}
return () => clearInterval(interval);
}, [isRunning, seconds]);
const formatTime = (s: number) => {
const mins = Math.floor(s / 60);
const secs = s % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const reset = () => {
setIsRunning(false);
setSeconds(25 * 60);
};
return (
<div className="timer">
<div className="timer-display">{formatTime(seconds)}</div>
<div className="timer-controls">
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? '⏸️ Pause' : '▶️ Start'}
</button>
<button onClick={reset}>🔄 Reset</button>
</div>
</div>
);
}
export default Timer;
Jangan lupa tambah permission untuk notifications:
{
"permissions": ["storage", "activeTab", "notifications"]
}
Kesimpulan
Membuat Chrome Extension dengan React dan Vite memberikan developer experience yang modern:
- Component-based UI dengan React
- Hot Module Replacement untuk development cepat
- TypeScript support out of the box
- Modern tooling dengan Vite
Extension yang kita buat sudah mencakup fitur-fitur penting:
- ✅ Popup UI dengan React
- ✅ Content scripts untuk modify halaman
- ✅ Background service worker
- ✅ Chrome Storage API
- ✅ Message passing antar komponen
Dari sini, kamu bisa expand extension dengan fitur tambahan: sync antar device, keyboard shortcuts, context menus, dan masih banyak lagi.
Resources
- Chrome Extension Documentation
- CRXJS Vite Plugin
- Chrome Extension Samples
- Manifest V3 Migration Guide
Happy building! 🚀