htmx 4.0 уже здесь
htmx 4.0 меняет наследование, подмену ошибок, историю и события, а также дает советы по миграции и откату для htmx 2.
htmx 4.0.0 вышел 28 августа 2026 года. Релиз меняет ряд давно устоявшихся значений по умолчанию: наследование атрибутов теперь работает только по явному запросу, ответы с ошибками подставляются в DOM, а кэш снимков истории исчез.
Если вы поддерживаете приложение на htmx 2, практический вопрос звучит так: продолжит ли hx-confirm, который вы два года назад вынесли на контейнер, что-либо защищать после обновления. Нет, не продолжит — если не добавить модификатор. В этой статье разбираем, что ломается, что откатывает каждое из изменений и почему стратегия публикации в npm означает, что действовать прямо на этой неделе, скорее всего, не придётся. О том, что такое htmx и зачем нужен гипермедиа-подход, рассказывает обзор htmx 2.0 — именно с этой точки начинается нынешний материал.
Ключевые выводы
- В htmx 4 наследование атрибутов становится явным благодаря модификатору
:inherited, а установкаhtmx.config.implicitInheritanceвtrueвозвращает поведение htmx 2 как мост на время миграции. - По умолчанию в htmx 4 подстановку пропускают только ответы
204и304, поэтому сформированный сервером422теперь попадает в целевой элемент, а не отбрасывается; вернуть прежнее поведение можно черезhtmx.config.noSwap = [204, 304, '4xx', '5xx']. - Имена событий следуют шаблону
htmx:phase:action, и для этого нет конфигурационного ключа: каждый слушатель htmx в вашем JavaScript придётся переименовать — либо установить расширениеhtmx-2-compat. - Переименуйте
hx-disableвhx-ignoreдо обновления, поскольку htmx 4 закрепляет имяhx-disableза задачей, которую раньше выполнялhx-disabled-elt. - Тег
latestв npm остаётся за htmx 2.x, а 4.0 публикуется подnext, поэтому URL CDN без указания версии не обновляются принудительно; в анонсе заявлено, что htmx 2 будет поддерживаться неограниченно долго.
Что изменилось в htmx 4?
htmx 4 переводит внутреннюю механику запросов с XMLHttpRequest на fetch(), и именно эта переработка сделала возможным всё остальное в релизе. Смена транспорта сама по себе уже была ломающим изменением, поэтому команда воспользовалась тем же мажорным релизом, чтобы сбросить значения по умолчанию, накопившиеся со времён htmx 1.
Ни один из этих API вы не вызываете напрямую, когда пишете на htmx, так что сама смена транспорта в шаблонах незаметна. Последствия проявляются по краям: специфичные для XHR события жизненного цикла не имеют эквивалента в fetch() и были удалены, а htmx 4 устанавливает htmx.config.defaultTimeout в 60000, тогда как htmx 2 позволял запросу висеть бесконечно.
Насчёт номера версии: создатель htmx Карсон Гросс заявлял, что htmx 3 не будет никогда, поэтому релиз перескакивает сразу на 4.0 — и обещание формально соблюдено. Свои доводы он изложил в эссе, анонсировавшем переработку, в ноябре 2025 года.
Наследование атрибутов теперь явное
В htmx 4 наследование происходит только тогда, когда вы об этом просите. Атрибут на контейнере действует исключительно на сам контейнер, если не добавлен модификатор :inherited, и этот модификатор работает с любым атрибутом: hx-boost:inherited, hx-target:inherited, hx-confirm:inherited.
<!-- htmx 4: the confirm reaches both buttons -->
<div hx-confirm:inherited="Are you sure?">
<button hx-delete="/account">Delete My Account</button>
<button hx-put="/account">Update My Account</button>
</div>
Значение на дочернем элементе по умолчанию перекрывает унаследованное. Если нужно объединить оба, используйте :append — именно этот сценарий композиции обычно и сбивает с толку:
<div hx-vals:inherited="tenant:acme">
<button hx-post="/save" hx-vals:append="source:save-btn">Save</button>
</div>
Без :append собственный hx-vals кнопки занимает место унаследованного, и tenant до сервера не доходит. Если ни один из предков атрибут не задаёт, добавленное значение оказывается единственным отправляемым. Названия немного различаются в зависимости от атрибута: на справочной странице hx-disable для той же задачи добавления к родительскому значению документирован :merge.
Атрибуты hx-inherit и hx-disinherit удалены — при явном подключении оба не нужны. Если ваши шаблоны опираются на старое поведение, установите htmx.config.implicitInheritance в true, чтобы восстановить его на время миграции. Относитесь к этому как к мосту, а не как к конечной точке.
Ответы с ошибками теперь подставляются по умолчанию
В htmx 4 ответ попадает в целевой элемент независимо от кода состояния, и задерживаются только 204 и 304. Сформированная сервером страница валидации с кодом 422 теперь попадает в целевой элемент, а не молча отбрасывается — именно этого гипермедиа-приложения и хотели всё это время. HTTP-ответ с ошибкой также порождает событие htmx:response:error.
Новый атрибут hx-status направляет отдельные коды в собственные цели и режимы подстановки:
<form hx-post="/submit"
hx-target="#result"
hx-status:422="target:#validation-errors"
hx-status:5xx="target:#server-error"
hx-status:503="swap:none">
<input name="email">
<button type="submit">Submit</button>
</form>
htmx сначала пробует наиболее конкретный шаблон: точный код, затем шаблон с замаскированной последней цифрой, например 50x, затем с двумя замаскированными — 5xx. Внутри значения атрибута можно задавать swap:, target:, select:, push:, replace: и transition:.
Если ваш бэкенд возвращает страницы ошибок, которые никогда не предназначались для подстановки, задайте htmx.config.noSwap равным [204, 304, '4xx', '5xx'] — и вы вернёте поведение htmx 2.
Навигация назад теперь настоящий запрос
htmx 4 отказывается от клиентского кэша DOM-снимков, на котором держалась история в htmx 2. Нажатие «назад» заставляет htmx снова запросить страницу у сервера, а затем подставить полученное в <body> либо в элемент [hx-history-elt], если такой на странице есть.
Практический эффект в том, что кнопка «назад» показывает страницу в том виде, в каком её сейчас отдаёт сервер, а не снимок, замороженный в момент ухода со страницы. Это убирает целый класс багов, когда сторонние скрипты изменяли DOM, а восстановленный снимок воспроизводил эти изменения, приводя интерфейс в сломанное состояние. Но это также означает, что навигация назад стоит одного запроса.
Атрибут hx-history исчез вместе с кэшем. Если снимки всё же нужны, основное расширение hx-history-cache возвращает их в качестве опциональной возможности.
Имена событий следуют шаблону htmx:phase:action
Все события жизненного цикла htmx переименованы к виду htmx:phase:action[:sub-action]. В анонсе приводятся htmx:beforeRequest → htmx:before:request и htmx:beforeSwap → htmx:before:swap; htmx:afterSwap превращается в htmx:after:swap.
Это единственное изменение без конфигурационной «лазейки». Править придётся каждый слушатель:
// htmx 2
document.body.addEventListener('htmx:afterSwap', (e) => {
initTooltips(e.detail.target);
});
// htmx 4
document.body.addEventListener('htmx:after:swap', (e) => {
initTooltips(e.detail.target);
});
Большинство событий об ошибках объединены в htmx:error, а HTTP-ответы с ошибками порождают htmx:response:error. Специфичные для XHR события попросту исчезли, поскольку у fetch() нет эквивалента. Если ручная правка слушателей составляет основную часть вашей миграции, расширение htmx-2-compat отображает старые имена событий на новые, а заодно возвращает неявное наследование и hx-ext.
Что в htmx 4 нового, а не сломанного?
Три нововведения htmx 4 сами по себе оправдывают обновление: morph-подстановки, элемент <hx-partial> и переписанные расширения для стриминга. Morph-подстановки теперь в ядре, так что обновления DOM с сохранением состояния больше не требуют расширения. Элемент <hx-partial> позволяет одному ответу обновить сразу несколько целей, причём каждая несёт собственные цель и режим подстановки:
<hx-partial hx-target="#messages" hx-swap="beforeend">
<div>New message</div>
</hx-partial>
<hx-partial hx-target="#count">
<span>5</span>
</hx-partial>
Поскольку цель и стиль подстановки заданы на самом partial, ответ сам сообщает, что делать с каждым фрагментом, вместо того чтобы заставлять вас вычислять это по атрибутам hx-swap-oob, разбросанным по разметке. Учтите, что порядок out-of-band в htmx 4 изменился на обратный: основное содержимое подставляется первым.
Второй ключевой пункт — расширения для стриминга. К этому релизу были перестроены и SSE-, и WebSocket-расширения, а рядом с ними выходит свежий набор: hx-multipart, hx-live, hx-targets, hx-ptag, hx-csp, hx-download, hx-prompt и hx-history-cache. Атрибуты подключения размещены в собственных пространствах имён, так что SSE подключается через hx-sse:connect, а WebSocket — через hx-ws:connect.
Переход на htmx 4 и почему спешить не нужно
Переход на htmx 4 начинается со сканера: запустите его прежде, чем что-либо планировать. npx htmx.org@4.0.0 upgrade-check -- ./path/to/project/root обходит проект и выводит каждый устаревший паттерн с именем файла и номером строки — этого достаточно, чтобы за полдня оценить объём работ.
npx htmx.org@4.0.0 upgrade-check -- ./templates
npx htmx.org@4.0.0 upgrade-check --ext .vue ./path/to/project/root
По умолчанию он просматривает .html, .php, .js, .ts, .jinja, .jinja2, .j2, .erb и .hbs. Форматы однофайловых компонентов в этот набор не входят, поэтому шаблоны .vue, .svelte, .jsx и .astro остаются непроверенными, если не передать --ext.
Одно переименование сделайте раньше всего остального: hx-disable становится hx-ignore, а hx-disabled-elt становится hx-disable. Старое имя переиспользовано для другой задачи, поэтому, если сначала мигрировать hx-disabled-elt, вы перезапишете атрибуты, которые ещё означают то, что означали в htmx 2.
| Изменение | Поведение htmx 4 по умолчанию | Что возвращает htmx 2 |
|---|---|---|
| Наследование атрибутов | Явное, через :inherited | htmx.config.implicitInheritance = true |
| Подстановка ответов с ошибками | Подстановку пропускают только 204/304 | htmx.config.noSwap = [204, 304, '4xx', '5xx'] |
| История | Повторный запрос к серверу при переходе назад | Расширение hx-history-cache |
| Имена событий | htmx:phase:action | Конфигурационного ключа нет; расширение htmx-2-compat |
И теперь то, что определяет, насколько всё это срочно: в npm dist-тег latest закреплён за htmx 2.x, а 4.0.0 опубликован под next. В анонсе прямо сказано, что это сделано намеренно, чтобы сайты, загружающие htmx по URL CDN без версии, не получили принудительное обновление с ломающими изменениями; 2.x останется latest до начала 2027 года. Поддержка 2.x сохраняется неограниченно долго.
Отсюда вытекают четыре ситуации. URL CDN без версии продолжает отдавать 2.x, пока тег не переключат, — это единственный случай с будущим дедлайном. Зафиксированный URL CDN и точная фиксация версии в npm сами по себе не изменятся никогда. Диапазон npm вида ^2.0.0 остаётся внутри 2.x независимо от dist-тегов. Чтобы установить 4.0 сегодня, зафиксируйте версию: npm install htmx.org@4.0.0 — или используйте путь CDN с указанием версии.
Новые проекты начинайте на 4.0. Для существующего приложения на htmx 2 запустите сканер, сначала выполните переименование hx-disable, а затем по длине отчёта решите, мигрировать сейчас или вернуться к этому до переключения dist-тега.
Частые вопросы
Как загрузить расширение htmx в htmx 4 теперь, когда hx-ext удалён?
Подключите скрипт расширения после скрипта htmx — его атрибуты заработают сразу, никакого активирующего атрибута не требуется. Загрузите dist/ext/hx-sse.js рядом с htmx.min.js, и вы сможете использовать hx-sse:connect напрямую. Сборка htmax.js поставляет htmx уже в комплекте с самыми популярными расширениями в одном файле, и эти атрибуты доступны автоматически. Авторы расширений регистрируют их через htmx.registerExtension, передавая имя и карту методов.
Можно ли избежать дополнительного запроса к серверу, который htmx 4 делает при навигации назад?
Да. Основное расширение hx-history-cache восстанавливает историю из sessionStorage вместо полноценного запроса к серверу — это ближайший аналог снимков htmx 2. Поведение также меняют два конфигурационных значения: htmx.config.history со значением 'reload' выполняет полную перезагрузку страницы при навигации по истории, а htmx.config.history со значением false отключает обработку истории. Кэша снимков в localStorage из htmx 2 больше нет.
Что пришло на смену hx-vars и hx-prompt в htmx 4?
hx-vars удалён, а вычисляемые значения переезжают в hx-vals с префиксом js:. hx-prompt вынесен из ядра и поставляется расширением: загрузите расширение hx-prompt, чтобы сохранить прежний синтаксис. Среди других удалённых атрибутов — hx-ext, hx-inherit, hx-disinherit и hx-history. hx-disabled-elt не удалён, а переименован: он становится hx-disable, а прежний hx-disable становится hx-ignore, как указано в таблице переименований в разделе [What's New in htmx 4](https://four.htmx.org/docs/whats-new-in-htmx-4). Сканер upgrade-check помечает эти два случая как renamed-attr, а настоящие удаления — как removed-attr, каждый раз указывая файл, номер строки и предлагаемую замену.
Работает ли hx-swap-oob в htmx 4 и когда вместо него стоит использовать hx-partial?
hx-swap-oob по-прежнему работает, но htmx 4 меняет порядок на обратный: сначала подставляется основное содержимое, а за ним в порядке следования в документе идут out-of-band-элементы и hx-partial. Обращайтесь к hx-swap-oob, когда заменяете один элемент обновлённой копией того же элемента, и к hx-partial, когда один ответ должен обновить несколько мест, поскольку каждый partial задаёт собственные hx-target и hx-swap, а не полагается на атрибуты, разбросанные по разметке.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k