惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Privacy & Cybersecurity Law Blog
Cloudbric
Cloudbric
GbyAI
GbyAI
T
Threatpost
Google DeepMind News
Google DeepMind News
Jina AI
Jina AI
The Hacker News
The Hacker News
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
I
Intezer
美团技术团队
S
Schneier on Security
I
InfoQ
Project Zero
Project Zero
S
SegmentFault 最新的问题
IT之家
IT之家
C
CXSECURITY Database RSS Feed - CXSecurity.com
C
CERT Recently Published Vulnerability Notes
博客园 - 司徒正美
Security Latest
Security Latest
G
Google Developers Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Cisco Talos Blog
Cisco Talos Blog
L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
Lohrmann on Cybersecurity
G
GRAHAM CLULEY
Engineering at Meta
Engineering at Meta
L
LangChain Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cisco Blogs
大猫的无限游戏
大猫的无限游戏
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Apple Machine Learning Research
Apple Machine Learning Research
雷峰网
雷峰网
V
V2EX
The Register - Security
The Register - Security
A
Arctic Wolf
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Tor Project blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Microsoft Security Blog
Microsoft Security Blog
Stack Overflow Blog
Stack Overflow Blog
Vercel News
Vercel News
Spread Privacy
Spread Privacy
H
Help Net Security
H
Heimdal Security Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Оттачиваем UI: 3 микроанимации в сервисе бронирования отелей
vadimbydanov · 2026-05-07 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение7 мин

Охват и читатели3

Кейс

Я дизайнер в сервисе бронирования отелей Моя Бронь. Расскажу про 3 анимации, которые мне нравятся и над которыми я долго работал.

Все три паттерна я подсмотрел в других продуктах: Airbnb, Wallet в Телеграме, Family. В каждом блоке расскажу, откуда взял и что поменял под наш контекст. Разбираю не только макеты, но и реализацию: конкретные значения cubic-bezier, длительности с обоснованием, плюс грабли с FLIP, Teleport и :key + .

1. Поисковая строка в шапке

На странице выдачи номеров удобно иметь поисковую строку с датами и городом под рукой:

  1. Полистал, понял что по ценам не подходит и решил выбрать другие даты.

  2. Открыто несколько вкладок на разные даты, когда выбираешь лучшее предложение и нужно не запутаться где, что, какие даты.

Многие платформы закрепляют вверху большое поисковое поле ~100px под основным хедером

На десктопе место по вертикали особенно дорого. Ширины у нас много, а по высоте место на мониторе ноутбука съедают адресная строка хрома и док бар. Итого остаётся ~600–700px полезного пространства. Из них ещё часть отнимает эта огромная шапка.

Мы поместили строку с поиском сразу в основную шапку, где логотип и всё остальное. Сделали её компактной, а при нажатии она плавно разворачивается в полноценную строку, сразу с открытым полем, на которое нажал.

Этот паттерн подсмотрел у Airbnb, у них поиск тоже в шапке. Когда я был там пользователем, у меня было ощущение «о, как удобно». Хотелось, чтобы и у нас было так.

В итоге мы выигрываем место для отелей. Пользователи пришли именно за ними, и они должны быть центральными на экране.

Как устроено

Технически это классический FLIP поверх «призрачного» слоя через Teleport. Сам компонент в шапке мы не анимируем. Во время анимации в <body> живёт его клон с position: fixed, который и едет из точки А в точку Б.

В DOM одновременно живут три экземпляра поисковой формы:

  1. Реальный свёрнутый. Он в шапке всегда, виден, ловит клик. Высота 36px.

  2. Реальный развёрнутый. visibility: hidden, pointer-events: none. Нужен только как измерительный слой, с него снимается финальный bounding rect.

  3. Призрак в <body>. position: fixed, высокий z-index. Это то, что пользователь видит во время анимации и то, на чём он потом работает.

Открытие: снимаем from со свёрнутого, монтируем призрак развёрнутого вида на место свёрнутого, переключаем layout шапки, снимаем to через два nextTick, инверсия через transform, запускаем transition: transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1). Параллельно opacity полей внутри едет с 0 в 1 за 180ms. Закрытие зеркально.

Одна неочевидная мелочь. При клике на «Город» нужно сразу сфокусировать input, иначе пользователь видит лишнее мигание. Но ref на input подъезжает асинхронно после монтажа призрака, так что мы крутим requestAnimationFrame-цикл до 20 кадров, пока input не появится в DOM.

Грабли

border-radius: inherit на компоненте внутри призрака требует, чтобы родительский контейнер призрака тоже наследовал радиус. Иначе при pending-анимации скругления спрямляются в плоские углы.

Pending-бордер реализован через псевдоэлемент ::after. На GPU-scale он мерцает. Пришлось при закрытии переносить анимацию рамки с псевдоэлемента на сам контейнер призрака, а внутренний ::after гасить.

На планшете нужно жёстко фиксировать ширины колонок в grid-template-columns. Без этого при FLIP-scale поля прыгают во время анимации, потому что 1fr пересчитывается.

prefers-reduced-motion. Обе ветки open/close при mql.matches пропускают FLIP полностью и просто переключают состояния. Без анимации для тех, кто её отключил в настройках ОС.

На мобилке FLIP не делаем вообще. Места там нет. Компактная строка превращается в pill-баттон, который открывает обычную полноэкранную модалку с поиском внутри.

Посмотреть вживую можно на примере поиска отелей в Москве

2. Анимации статусов заказа

После оплаты пользователь попадает на страницу заказа. В этот момент мы отправляем запрос на создание брони нашему вендору, и дальше возможны три исхода:

  1. Бронь подтвердят быстро, 5–15 секунд

  2. Отель отклонит бронь: овербукинг или иные причины

  3. Уйдёт на ручное подтверждение отелем, может занять часы

Пользователь в это время сидит на странице и смотрит. Хочется создать ощущение плавности и непрерывности взаимодействия, что процесс продолжается. Если не анимировать и просто написать «Ждем подтверждение отеля...», это выглядит как зависание, и рука тянется рефрешить страницу. Поэтому мы делаем плавные переходы. Спиннер мягко превращается в галочку или крест, тексты статусов и высота карточки тоже меняется плавно.

Вдохновлялся криптокошельком Family. У них здорово показаны этапы отправки перевода в блокчейне: принят, в ожидании, подтверждён. И параллельно на ByBit, например, после отправки перевода приходится обновлять страницу, чтобы увидеть выход из pending. Вот такого ощущения «зависло, надо обновлять» хотелось избежать.

Что анимируем

Четыре отдельных движения, которые работают одновременно.

Иконка статуса. Lottie-анимация. Loading в цикле, при смене статуса проигрывается «выход»: loading-to-check или loading-to-cross. Важная мелочь: новую иконку ставим после окончания loading-анимации, не во время. Иначе виден шов между лупом и переходом.

Заголовок. Когда иконка меняется с одной строки на две, заголовок плавно подъезжает по вертикали, чтобы остаться на одной линии с её центром. 0.3s, cubic-bezier(0.32, 0.94, 0.60, 1).

Сам текст статуса. 3D-перелистывание: старый текст уходит вниз с rotateX 90°, новый прилетает сверху с тем же rotateX. Как будто пластинка с текстом физически переворачивается. Приём подсмотрел в TG Wallet, там так же крутятся строки на экране загрузки поиска P2P обмена. 3D вместо обычного fade — интереснее выглядит, плюс лейаут не дёргается: текст крутится в одной строке, не наезжает на элементы другие. Длительность 0.3s, easing cubic-bezier(0.76, 0, 0.24, 1).

Высота карточки. Контент внутри меняется. Где-то CountdownTimer, где-то список «оплачено / ждём подтверждения / подтверждено», где-то ничего. Высота едет за контентом плавно, лишнее обрезается через overflow: hidden. 0.3s, cubic-bezier(0.32, 0.94, 0.60, 1).

Как устроено (на примере Telegram miniapp)

Никаких стейт-машин и оркестраторов, просто Vue-реактивность. Здесь три-четыре состояния и линейный переход между ними, XState или подобное было бы избыточно. У нас страница поллит бэк каждые 5 секунд, свежие данные пишутся в рефы, computed собирает текущий статус.

Вся анимация смены в одной строчке:

<TransitionHeight>
  <StatusInfoView :key="keyStatus" v-if="statusInfo.list" :list="statusInfo.list" />
</TransitionHeight>

keyStatus — это конкатенация ключевых полей статуса. Любое поле изменилось, :key поменялся, Vue считает компонент новым. Старый делает leave (fade + collapse), новый enter. Никакой ручной координации, никаких таймеров.

<TransitionHeight> внутри — это обёртка над <Transition>, которая замеряет реальную высоту нового слота и анимирует height от 0 до неё.

Полезное

window.changeStatus и window.changeMethodPay в onMounted для отладки. Из консоли вручную переставить статус, и сразу видно анимацию. Не нужно ждать нужное состояние от бэка. На настройке тестовых сценариев сэкономили несколько часов.

Посмотреть вживую можно забронировав отель в приложении или тг-миниапе Моя Бронь (оплачивать бронь не обязательно).

3. Анимация загрузки на странице поиска

Мы платформа по поиску и бронированию отелей, и поиск должен работать хорошо. Ценность сервиса в том, что тут удобно искать. А удобно — это быстро. Если страница грузится минуту, никакая анимация не спасёт. Хочется закрыть и забыть.

В прошлой статье я рассказывал, как у нас устроен кэш поиска. Кэшируем на 2 недели вперёд, и от «нажал Найти» до «увидел результаты» проходит меньше 7 секунд. Но есть запросы на дальние даты, которые мы не кэшируем. Там идём прямо к вендору, и поиск занимает 15–20 секунд.

Вот эти 15–20 секунд — наша задача. Хочется развлечь пользователя на это время: чтобы что-то крутилось, менялось, и не выглядело, что приложение зависло. Поэтому у нас сверху прогрессбар, а по центру лупа и меняющиеся тексты. Много движения, чтобы экран выглядел рабочим.

Что анимируем

Меняющиеся тексты. Массив из i18n: «Подбираем лучшие отели», «Проверяем рейтинг отелей», «Ищем свободные места». Меняются каждые 3 секунды.

Сама смена текста — тот же 3D-барабан, что и при смене статусов заказа в прошлом блоке.

Технически это тот же :key + <Transition>, что и в блоке статусов. Специфичная мелочь: на enter-active и leave-active нужно ставить position: absolute. Иначе на 0.6 секунды лейаут прыгает: старый текст ещё уезжает, а новый уже занял место. Контейнер при этом делаем position: relative с фиксированной высотой строки.

Прогрессбар сверху. Тонкая линия 2px, ползёт слева направо. Поверх бегает белый блик.

Прогрессбар калиброванный, а не реальный

Настоящий прогрессбар тут невозможен в принципе. Бэкенд опрашивает вендора, а вендор не отдаёт ни количества пройденных шагов, ни ETA. Мы не знаем заранее, найдёт ли он 50 отелей или 5000, и не знаем, ответит за 5 секунд или за 25. Любая «честная» индикация либо стояла бы мёртвой строкой первые секунды, либо прыгала бы рывками от 0% к 100% в момент прихода ответа. Поэтому мы не индицируем прогресс — мы его имитируем.

Сама имитация максимально простая. setInterval каждые 150 мс наращивает значение на 1%:

const timer = setInterval(() => {
  progress.value++;
}, 150);

Шкала подобрана под типичное время поиска: 150мс × 100 = 15 секунд, медианный ответ приходит за 15–20. В среднем бар добегает до 100% близко к моменту ответа, в хвостовых случаях стоит до 5 секунд на сотке. Это компромисс с альтернативой — асимптотическим замедлением около 90%. Мы выбрали честный паркинг на 100% вместо непредсказуемого jitter.

Пользователь видит движение и не воспринимает ожидание как «зависло». Реальные ориентиры здесь — Lottie с лупой и ротация текстов. Они работают независимо от того, сколько идёт ответ. Прогрессбар — это третий элемент движения, который усиливает ощущение «оно работает».

Когда ответ приходит, clearInterval, прогресс долетает до 100, через 0.75 секунды паузы (видно, как полоса достигла конца) полоса плавно гаснет. Пользователь видит логичное завершение и сразу список отелей.

Концепт: цитаты русских писателей вместо служебных текстов

В играх на долгих экранах загрузки часто показывают советы или цитаты, и это работает. У нас философия проекта — российский, с душой, сделанный людьми. Хочется и загрузку с душой, вместо «проверяем рейтинг», цитаты русских писателей про отели и гостиницы. Чтение в момент ожидания приближает к высокому. Пока не внедрили, лежит в папке «когда дойдут руки».