Минимальное расширение Chrome состоит из manifest.json, окна popup.html и скрипта popup.js. Кнопка может менять текущую вкладку через chrome.scripting. Мы сделаем режим фокуса: нажатие приглушает изображения и боковые блоки открытой страницы, повторное нажатие возвращает их. Разрешение activeTab даст временный доступ только после действия пользователя.
Расширение удобно изучать на маленьком эффекте. Не нужен сервер, учётная запись или публикация в магазине: Chrome умеет загрузить папку прямо с диска. При этом вы сразу встречаете настоящую архитектуру платформы. Popup живёт в своём документе, а страница во вкладке в другом. Обычный document.querySelector() из popup ищет элементы только внутри popup, поэтому код для страницы нужно выполнить через API расширений.
Manifest сообщает Chrome имя, версию, разрешения и файл popup.
Popup содержит одну кнопку и короткий статус для пользователя.
chrome.scripting запускает небольшую функцию в активной вкладке после клика.
Из каких файлов состоит первое расширение
Создайте пустую папку focus-extension. Manifest V3 является точкой входа: Chrome сначала читает JSON и узнаёт, какой интерфейс открыть. Мы запросим scripting для выполнения функции и activeTab для временного доступа к вкладке после жеста пользователя. Широкое разрешение на все сайты для такого примера не требуется.
| Файл или разрешение | Роль | Что проверить |
|---|---|---|
manifest.json | Метаданные и возможности | Корректный JSON без комментариев |
popup.html | Маленькое окно по значку | Есть кнопка с id focus |
popup.js | Обработчик и вызов API | Скрипт подключён отдельным файлом |
scripting | Выполнение функции во вкладке | Указан в массиве permissions |
activeTab | Временный доступ после клика | Нет лишнего доступа ко всем URL |

Manifest связывает окно popup с разрешениями, а JavaScript выполняет действие после клика.
// объяснение делает manifest невалидным. Пояснения держите в README, а ошибки загрузки читайте на карточке расширения.Пишем manifest, popup и функцию для вкладки
Создайте три файла из примера. В manifest укажите Manifest V3 и popup. В HTML подключите JavaScript через src: встроенный обработчик в атрибуте кнопки нарушает ограничения безопасности расширений. Функция toggleFocusMode не зависит от переменных popup, потому что будет сериализована и выполнена в контексте страницы.
{
"manifest_version": 3,
"name": "Режим фокуса",
"version": "1.0.0",
"action": { "default_popup": "popup.html" },
"permissions": ["activeTab", "scripting"]
}
<!-- popup.html -->
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<style>
body { width: 240px; padding: 16px; font: 15px system-ui; }
button { width: 100%; padding: 12px; border: 0; border-radius: 12px;
color: white; background: #4f46e5; cursor: pointer; }
#status { min-height: 22px; }
</style>
</head>
<body>
<button id="focus">Переключить режим фокуса</button>
<p id="status" aria-live="polite"></p>
<script src="popup.js"></script>
</body>
</html>
// popup.js
const button = document.querySelector('#focus');
const status = document.querySelector('#status');
function toggleFocusMode() {
const styleId = 'codik-focus-mode';
const oldStyle = document.getElementById(styleId);
if (oldStyle) {
oldStyle.remove();
return 'Режим фокуса выключен';
}
const style = document.createElement('style');
style.id = styleId;
style.textContent = 'img, video, aside { opacity: .12 !important; }';
document.head.append(style);
return 'Режим фокуса включён';
}
button.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const [{ result }] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: toggleFocusMode,
});
status.textContent = result;
});- Popup находит активную вкладку в текущем окне и получает её
tab.id. executeScriptвыполняет функцию внутри страницы и возвращает её текстовый результат.- Первый клик добавляет стиль с постоянным id, второй находит этот стиль и удаляет его.

DOM popup и DOM страницы разделены, поэтому действие проходит через API расширений.
Почему activeTab безопаснее широкого доступа
Расширение может запросить доступ к определённым сайтам через host permissions, но для кнопки, которую пользователь сознательно нажимает на текущей странице, достаточно activeTab. Такое разрешение выдаёт временный доступ после жеста, например нажатия на значок или команды. Доступ заканчивается при закрытии вкладки или переходе на другой origin. Системные страницы браузера всё равно защищены.
| Подход | Доступ | Когда подходит |
|---|---|---|
activeTab | Текущая вкладка после действия | Разовая кнопка и локальный эффект |
| Host permission | Заранее указанные адреса | Функция должна работать на них постоянно |
<all_urls> | Почти все страницы | Только при ясной необходимости и объяснении |
| Системная страница Chrome | Внедрение запрещено | Покажите понятное сообщение вместо падения |
Практика: загружаем распакованное расширение
Откройте страницу управления расширениями, включите режим разработчика и загрузите всю папку проекта. После правки manifest или JavaScript нажимайте кнопку перезагрузки на карточке расширения. Popup закрывается при потере фокуса, поэтому постоянные данные позже стоит хранить отдельно, но наш стиль уже живёт в самой вкладке до обновления страницы.
- Скопируйте JSON до маркера
popup.htmlв manifest и сохраните три файла в одной папке. - Откройте
chrome://extensions, включите Developer mode и нажмите Load unpacked. - Выберите папку
focus-extensionи закрепите новый значок на панели. - Откройте обычную статью по HTTP или HTTPS, нажмите значок и кнопку режима фокуса.
- Убедитесь, что изображения и боковые элементы стали прозрачнее, а статус сообщил о включении.
- Нажмите второй раз и проверьте возврат страницы в исходный вид.
- Откройте ошибки карточки расширения и консоль popup. Там не должно быть красных сообщений.
- Chrome принимает manifest и показывает значок расширения без предупреждения о невалидном JSON.
- Popup открывается, кнопка реагирует и не требует обновлять саму страницу.
- Один и тот же клик попеременно добавляет и удаляет стиль только в активной вкладке.
- Расширение не запрашивает постоянный доступ ко всей истории сайтов ради одной кнопки.

Manifest, popup, разрешения и страница оставляют ошибки в разных местах.
Почему кнопка popup ничего не делает
Диагностика расширения разделена на два контекста. Ошибки кнопки и вызова API ищите в консоли popup. Ошибки функции, выполненной во вкладке, могут появиться в DevTools самой страницы. Кроме того, Chrome не разрешает внедрение в некоторые внутренние страницы. Поэтому тестируйте на обычном сайте и оборачивайте пользовательский сценарий понятным сообщением об ошибке.
Кнопка видна, но обработчика нет. Проверьте имя файла, тег script и ошибку 404 в инспекторе popup.
Вызов chrome.scripting.executeScript завершается ошибкой разрешения.
У каждого документа свой DOM. Передайте функцию через scripting, если нужно изменить открытую вкладку.
Защищённая системная страница не разрешает обычное внедрение. Откройте публичный сайт.
Chrome продолжает использовать старый пакет. Нажмите Reload на карточке и заново откройте popup.
activeTab, не заменяйте его на широкий доступ «на всякий случай». Пользователь видит набор разрешений и принимает решение об установке. Технические детали сверены с документацией Chrome по chrome.scripting.Зачем нужен manifest.json?
Он сообщает Chrome версию формата, имя, версию расширения, popup и разрешения.
Почему document.querySelector из popup не видит страницу?
Popup и вкладка являются разными документами с отдельными DOM.
Что даёт activeTab?
Временный доступ к активной вкладке после осознанного действия пользователя.
Нужен ли доступ ко всем сайтам?
Нет. Для разового эффекта после клика достаточно activeTab и scripting.
Почему пример не работает на chrome://?
Chrome защищает внутренние страницы и не разрешает обычным расширениям внедрять туда скрипт.
События, функции и работа с DOM подробно закрепляются в курсе JavaScript. Если связь файлов пока путается, сначала откройте инструкцию подключения JavaScript к HTML.
Следующий шаг удобно построить на манипуляциях с DOM: меняйте размер текста, скрывайте выбранные элементы или считайте заголовки. Для хранения настроек используйте идеи из проекта списка задач, а маршрут по базе фронтенда держите в хабе HTML, CSS и JavaScript.