Как сделать кнопку «Скопировать» — и почему она молчит на http

Кнопка «Скопировать» под ответом ИИ-чата или под блоком кода — это одна строка JavaScript. Ты пишешь её, проверяешь на своём компьютере — работает. Выкладываешь на тестовый сервер, открываешь по IP-адресу — кнопка молчит. А в консоли:
TypeError: Cannot read properties of undefined (reading 'writeText')
Код не сломан. Просто navigator.clipboard существует только в защищённом контексте. На обычном http его нет вовсе. Соберём кнопку, которая работает везде, где может, и честно говорит, когда не может.
Что понадобится
- Страница по HTTPS или на localhost. MDN считает
http://localhostзащищённым — поэтому локально всё и работало. - Текст, который нужно копировать.
- Пятнадцать строк JavaScript. Библиотека не нужна: метод
writeTextесть в Chrome с версии 66, в Firefox с 63 и в Safari с 13.1.
Шаг 1. Разметка
<pre id="answer">npm install zod</pre>
<button type="button" class="copy" data-target="answer">Скопировать</button>
<span class="copy-status" role="status"></span>
Три детали. type="button" — чтобы внутри формы кнопка не отправляла её. data-target — откуда брать текст. А пустой span с role="status" нужен для шага 4. Он должен быть в HTML с самого начала, а не появляться потом.
Шаг 2. Копирование
document.querySelectorAll('.copy').forEach((button) => {
button.addEventListener('click', async () => {
const text = document.getElementById(button.dataset.target).textContent;
const status = button.nextElementSibling;
try {
await navigator.clipboard.writeText(text);
status.textContent = 'Скопировано';
} catch (error) {
status.textContent = 'Не вышло — выдели текст и нажми Ctrl+C';
}
setTimeout(() => { status.textContent = ''; }, 2000);
});
});
writeText возвращает промис: копирование асинхронное, поэтому await. Если браузер запретит запись, промис упадёт с ошибкой NotAllowedError. А если navigator.clipboard нет совсем, try поймает и эту ошибку. В обоих случаях человек получит понятную подсказку вместо тишины.
textContent берёт текст ровно как в коде, со всеми пробелами и переносами. Для блоков кода это именно то, что нужно.
Шаг 3. Копируй прямо в обработчике клика
Браузеры пускают в буфер обмена не когда угодно, а сразу после действия человека. Это называется временной активацией. В Firefox и Safari без неё запись не пройдёт вовсе. Chromium пускает либо по активации, либо по разрешению clipboard-write.
Отсюда две ошибки:
- Копировать при загрузке страницы или по таймеру. Человек ничего не нажимал — буфер закрыт.
- Сначала долго ждать ответ сервера, а потом копировать. Активация может истечь, особенно в Safari.
Решение простое: подготовь текст заранее. В обработчике клика — только writeText, без долгих запросов перед ним.
Шаг 4. Подтверждение, которое увидят и услышат
Надпись «Скопировано» на две секунды — хорошо для глаз. Но незрячий человек её не увидит. Поэтому статус лежит в элементе с role="status". Когда текст в нём меняется, скринридер спокойно, не перебивая, произносит «Скопировано».
Не заменяй подтверждение только сменой иконки или цвета. Галочку вместо значка копирования скринридер не озвучит, а человек с плохим зрением её может не различить.
Шаг 5. Если кнопка живёт внутри iframe
Виджет чата, встраиваемый блок кода, демо на чужом сайте — всё это iframe. В Chromium-браузерах фрейму нужно явно разрешить запись:
<iframe src="https://widget.example.com" allow="clipboard-write"></iframe>
Без этого writeText внутри фрейма упадёт с NotAllowedError, даже по клику.
Что получится
Кнопка под любым блоком текста или кода. Клик — текст в буфере — надпись «Скопировано» на две секунды, которую скринридер произносит вслух. На http без localhost человек увидит подсказку «выдели и нажми Ctrl+C», а не мёртвую кнопку.
Для ИИ-чата добавь одно правило. Если ответ приходит потоком, держи кнопку неактивной, пока поток не закончится. Иначе человек нажмёт на середине и скопирует половину ответа.
Почему не копирует: три частые причины
- Страница открыта по
httpне на localhost.navigator.clipboardравенundefined. Проверка: набери в консолиwindow.isSecureContext— должно бытьtrue. - Копирование вызвано не из действия человека. Загрузка страницы,
setTimeout, долгий запрос перед записью. - Кнопка во фрейме без
allow="clipboard-write". Проверка: та же кнопка вне фрейма работает.
А старый способ через document.execCommand('copy') MDN помечает как устаревший. Для нового кода — только Clipboard API.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.
Можно ли скопировать картинку, а не текст?
Да, через navigator.clipboard.write() и объект ClipboardItem. Надёжнее всего копировать PNG: этот формат поддерживают Chrome, Firefox и Safari.
Может ли сайт прочитать мой буфер обмена без спроса?
Нет. Для чтения нужно действие человека. Chrome дополнительно спросит разрешение, а Firefox и Safari покажут маленькое меню с кнопкой «Вставить».
Почему вместе с кодом копируются номера строк?
Потому что номера нарисованы отдельными элементами внутри блока, и textContent забирает их вместе с кодом. Храни чистый текст отдельно, например в атрибуте data-code у кнопки, и копируй его. Или рисуй номера через CSS-счётчики: такой текст в буфер не попадает.
Как сделать то же самое в React?
Логика та же, меняется только место, где живёт статус:
function CopyButton({ text }) {
const [status, setStatus] = useState('');
async function copy() {
try {
await navigator.clipboard.writeText(text);
setStatus('Скопировано');
} catch {
setStatus('Не вышло — выдели текст и нажми Ctrl+C');
}
setTimeout(() => setStatus(''), 2000);
}
return (
<>
<button type="button" onClick={copy}>Скопировать</button>
<span role="status">{status}</span>
</>
);
}
Текст приходит пропсом, а не читается из DOM, — поэтому лишнее вроде номеров строк не попадёт в буфер вовсе.





