Что теперь умеют сообщения в Telegram: кнопки, анимации и свой JavaScript
- среда, 11 ноября 2026 г. в 00:00:12
В сообщение Telegram теперь можно засунуть небольшую веб-страницу со своим JavaScript. Получатель открывает чат и сразу может с ней взаимодействовать: нажимать кнопки, двигать элементы, играть. Код выполняется прямо внутри сообщения, а HTML и стили бот отправляет вместе с ним.
Это новые HTML-виджеты. На весь код дают всего 4 КБ после сжатия, и мне стало интересно, что в них вообще поместится. Оказалось, даже игра.
Для эксперимента я собрал башню: нажимаешь на сообщение, очередной этаж падает вниз. Если поставил криво, торчащий кусок отваливается. Отправил её себе в Telegram, потыкал. Вся игра заняла чуть больше 3 КБ после сжатия.

Бот может прислать новые данные в уже отправленный виджет. Скрипт получит их и обновит текст или другие элементы сообщения, если заранее написать для этого обработчик.
Давайте возьмём классический пример: пиццерия принимает заказы через Telegram. Банально, зато на нём удобно сравнить обычные сообщения бота, Mini App и новые виджеты.
Покупатель выбрал «Маргариту», решил взять две и хочет увидеть итоговую сумму.
Кнопка «Добавить в заказ» стоит под сообщением. Когда на неё нажимают, бот получает событие. Если нужно показать новую сумму, бот редактирует сообщение или присылает следующее.

Mini App по сути открывает ваш сайт во встроенном браузере Telegram, на отдельном экране. Вы размещаете страницу на сервере, а Telegram загружает её по URL. Для нашей пиццерии это может быть каталог с корзиной и формой доставки.
У Mini Apps есть API для интеграции с Telegram: можно получить данные пользователя, подстроить цвета под тему мессенджера или использовать его кнопки. Сам интерфейс вы пишете на привычных HTML, CSS и JavaScript.

Жмёте «+», и сумма меняется прямо в карточке. Её пересчитывает скрипт внутри сообщения. Когда нажмёте «Оформить заказ», виджет обратится к боту, чтобы тот записал заказ на сервере.

Обновление с сервера я пока не пробовал, поэтому этот пример разобрал по документации Telegram. Допустим, покупатель оформил заказ. Бэкенд пиццерии сохранил его под номером 42 со статусом «Принят», и теперь бот отправляет покупателю карточку.
Карточка хранится в файле order-widget.html. В разметке есть поля для номера и статуса заказа. JavaScript внутри <script> заполняет эти поля при открытии виджета и обновляет их, когда бот присылает новые данные.
<p>Заказ №<span id="order-id">…</span></p> <p>Статус: <strong id="order-status">Загрузка…</strong></p> <script> const widget = window.TelegramWidget; const orderId = document.getElementById('order-id'); const orderStatus = document.getElementById('order-status'); function renderOrder() { try { const order = JSON.parse(widget.chatData || 'null'); orderId.textContent = order?.id ?? '—'; orderStatus.textContent = order?.status ?? 'Нет данных'; } catch { orderStatus.textContent = 'Ошибка данных'; } } widget.onEvent('chatDataChanged', renderOrder); renderOrder(); </script>
Объект window.TelegramWidget предоставляет Telegram. Его свойство chatData содержит строку с данными этого сообщения. Функция renderOrder() разбирает JSON и записывает номер заказа в order-id, а статус в order-status. В конце скрипт подписывается на событие chatDataChanged и вызывает renderOrder() первый раз, чтобы сразу показать заказ.
В нашем примере бэкенд написан на Node.js. Он читает HTML из файла, а токен бота из переменной окружения TELEGRAM_BOT_TOKEN. Покупатель общается с ботом в личном чате с условным ID 123456789. Его заказ №42 уже принят.
import { readFile } from 'node:fs/promises'; const token = process.env.TELEGRAM_BOT_TOKEN; const html = await readFile('./order-widget.html', 'utf8'); async function callTelegram(method, params) { const response = await fetch( `https://api.telegram.org/bot${token}/${method}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }, ); const data = await response.json(); if (!response.ok || !data.ok) { throw new Error(data.description || 'Ошибка Telegram API'); } return data.result; } async function sendOrderWidget(chatId, order) { return callTelegram('sendWidget', { chat_id: chatId, html, chat_data: JSON.stringify(order), }); } // Пример заказа, который бэкенд уже принял. const order = { id: 42, status: 'Принят' }; const chatId = 123456789; // Условный ID чата покупателя. const message = await sendOrderWidget(chatId, order);
Функция sendOrderWidget() отправляет в Telegram разметку в поле html и строку {"id":42,"status":"Принят"} в поле chat_data. Статус «Принят» задан в объекте order на сервере. JavaScript в виджете прочитает его через widget.chatData и покажет покупателю.
Telegram возвращает отправленное сообщение, и код сохраняет его в message. Поля message.chat.id и message.message_id указывают, где находится карточка заказа №42. По ним бот найдёт её при обновлении статуса.
Кухня взяла заказ в работу. Бэкенд меняет статус в order на «Готовится» и вызывает editMessageWidgetData через функцию callTelegram. ID чата и сообщения он берёт из сохранённого message.
// Позже, когда кухня взяла этот заказ в работу. order.status = 'Готовится'; await callTelegram('editMessageWidgetData', { chat_id: message.chat.id, message_id: message.message_id, chat_data: JSON.stringify(order), });
Telegram обновляет chatData у выбранного сообщения и вызывает событие chatDataChanged. Наш JavaScript снова запускает renderOrder(), и надпись меняется на «Готовится». HTML-файл повторно отправлять не нужно. Чтобы изменить сам код виджета, придётся прислать новый.
Вся разметка, стили и JavaScript должны уложиться в 32 768 байт исходника и 4 096 байт после сжатия. Поэтому к карточке пиццы уже не так просто добавить фотографии. Загрузить их по URL нельзя, а встроенные в HTML изображения тоже занимают место в этих четырёх килобайтах.
JavaScript выполняется в QuickJS без доступа к сети. Например, скрипт карточки не сможет сам запросить актуальную цену через fetch. За внешними данными ему придётся обращаться к боту. Это повлияет на то, какую логику пиццерия сможет перенести в сообщение.
С вводом адреса доставки тоже возникнет проблема. Telegram удаляет из разметки input, textarea и select, а клавиатура внутри виджета недоступна. Количество пицц можно выбрать кнопками, но для ввода улицы и номера квартиры понадобится другой интерфейс. В нашем примере форму доставки можно оставить в Mini App, а в переписке показывать карточку заказа со статусом.
Когда башня заработала, я попытался переслать её в канал. Telegram не дал. В публичном Bot API виджеты пока работают в личке с ботом, а пересылка отключена. Если хотите потыкать свой HTML в чате, отправьте его себе через sendWidget. Подписчикам канала пока можно показать запись или дать ссылку на бота.
Мне не терпится увидеть, что люди с этим сделают. Уже хочется открывать чужие эксперименты, тыкать во всё подряд и разбираться, как это вообще уместили в одном сообщении.