Команда JavaScript for Devs подготовила перевод большого разбора новых CSS-возможностей, появившихся в Safari 26. Это, пожалуй, один из самых заметных релизов браузера за последние годы: поддержка anchor positioning, анимаций, зависящих от прокрутки, функции progress(), улучшенное абсолютное выравнивание, contrast-color() и даже «красивые» переносы текста.


Не так давно команда Apple выпустила Safari 26.0! Насколько это важно? Казалось бы, браузеры всё время выходят с новыми версиями и подсыпают по паре новых возможностей. Они, конечно, полезны, но между версиями редко бывают «большие скачки». Однако Safari 26 — другое дело. Здесь действительно много всего. Точнее: добавлено 75 новых возможностей, 3 пометки как устаревшие и ещё 171 улучшение.

По-моему, это по-настоящему серьёзное обновление.

Пост в блоге WebKit отлично разбирает каждую из новых (и не только CSS) возможностей. Но их столько, что нововведения в CSS заслуживают отдельного внимания. Поэтому сегодня я хочу посмотреть (и заодно попробовать) те из них, которые кажутся мне самыми интересными в Safari.

Что нового (для Safari)?

Safari 26 приносит несколько возможностей, которые вы могли уже видеть в прежних релизах Chrome. И… сложно упрекать Safari за видимое отставание — Chrome выпускает новый CSS пугающе быстрыми темпами. Мне нравится, что браузеры выпускаются «вразнобой»: так у них есть время «отшлифовать» специфику друг друга. Помните, как Chrome изначально выпустил position-area под именем inset-area? В итоге между реализациями появилось более удачное именование.

Думаю, вы тоже заметите (как и я), что многие из этих пересекающихся возможностей — часть большого направления Interop 2025, которому WebKit активно следует. Так что давайте посмотрим, что именно нового в Safari 26… по крайней мере, нового для самого Safari.

Позиционирование по якорям

Позиционирование по якорям — одна из моих любимых возможностей (я писал гайд по ней!), так что я очень рада, что она добралась до Safari. Мы на шаг ближе к широкодоступной поддержке, а значит — к тому, чтобы использовать anchor positioning в продакшене.

С CSS Anchor Positioning мы можем «прицепить» абсолютно позиционированный элемент (назовём его «target») к другому элементу (пусть это будет «anchor»). Это делает создание тултипов, модалок и попапов элементарным на чистом CSS — хотя приёмы подходят и для самых разных раскладок.

С помощью anchor positioning можно связать любые два элемента, вроде этих, друг с другом. И вовсе не важно, где они находятся в разметке.

<div class="anchor">anchor</div>
<div class="target">target</div>

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

Мы регистрируем элемент .anchor с помощью свойства anchor-name, которое принимает идентификатор с дефисами (dashed ident). Затем используем этот идентификатор, чтобы «прикрепить» .target к .anchor через свойство position-anchor.

.anchor {
  anchor-name: --my-anchor; /* the ident */
}

.target {
  position: absolute;
  position-anchor: --my-anchor; /* attached! */
}

Это размещает .target по центру .anchor — снова, независимо от порядка в исходной разметке! Если нужно поместить его в другое место, самый простой способ — использовать свойство position-area.

С position-area мы можем определить область вокруг .anchor и расположить в ней .target. Представьте, что вы рисуете сетку из квадратов, привязанную к центру, верхнему, правому, нижнему и левому краям .anchor.

Например, если мы хотим поместить target в правый верхний угол anchor, можно написать…

.target {
  /* ... */
  position-area: top right;
}

Это лишь небольшая демонстрация — позиционирование по якорям само по себе целый мир. Рекомендую прочитать наше полное руководство по теме.

Анимации, зависящие от прокрутки

Анимации, зависящие от прокрутки, связывают CSS-анимации (созданные через @keyframes) с позицией прокрутки элемента. То есть вместо запуска анимации на заданное время её ход зависит от того, куда прокручивает пользователь.

Мы можем привязать анимацию к двум типам событий прокрутки:

  • К прокручиваемому контейнеру с помощью функции scroll().

  • К положению элемента во вьюпорте с помощью функции view().

Обе эти функции используются в свойстве animation-timeline, которое «подключает» прогресс анимации к выбранной временной шкале — scroll или view. В чём разница?

С scroll() анимация идёт по мере прокрутки страницы. Простейший пример — индикатор чтения, который «растёт» по мере продвижения вниз. Сначала мы задаём обычную анимацию и применяем её к полосе.

@keyframes grow {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

.progress {
  transform-origin: left center;
  animation: grow linear;
}

Я устанавливаю transform-origin: left, чтобы анимация шла слева направо, а не расширялась от центра.

Затем вместо того, чтобы задавать длительность, подключаем анимацию к позиции прокрутки вот так.

.progress {
  /* ... */
  animation-timeline: scroll();
}

Если вы используете Safari 26 или последнюю версию Chrome, полоса будет увеличиваться по ширине слева направо по мере прокрутки вьюпорта.

Функция view() похожа, но основывает анимацию на положении элемента, когда он попадает в область видимости вьюпорта. Так анимация может начинаться или заканчиваться в конкретных точках страницы. Вот пример, в котором изображения «выпрыгивают», когда входят в поле зрения.

@keyframes popup {
  from {
    opacity: 0;
    transform: translateY(100px);
  }
  to {
    opacity: 1;
    transform: translateY(0px);
  }
}

img {
  animation: popup linear;
}

Затем, чтобы анимация продвигалась по мере попадания элемента во вьюпорт, подключаем animation-timeline к view().

img {
    animation: popup linear;
    animation-timeline: view();
}

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

Здесь нам пригодится свойство animation-range. Оно позволяет задать начало и конец анимации относительно вьюпорта. В этом примере я хочу, чтобы анимация начиналась, когда элемент входит на экран (то есть с отметки 0%), и заканчивалась чуть раньше, чем он достигнет центра вьюпорта (скажем, на 40%):

img {
  animation: popup linear;
  animation-timeline: view();
  animation-range: 0% 40%;
}

Ещё раз: анимации, зависящие от прокрутки, выходят далеко за рамки этих двух базовых примеров. Для быстрого введения во все их возможности рекомендую заметки Джеффа.

Мне спокойнее использовать анимации, зависящие от прокрутки, в продакшене, потому что это скорее прогрессивное улучшение: даже если браузер их не поддерживает, опыт не ломается. При этом у некоторых пользователей может быть предпочтение к уменьшенной (или вовсе отсутствующей) анимации, так что всё равно стоит применять прогрессивное улучшение с prefers-reduced-motion.

Функция progress()

Ещё одна возможность, появившаяся в Chrome и добравшаяся до Safari 26. Забавно, я пропустил её в Chrome, когда она вышла несколько месяцев назад, так что вдвойне рад видеть столь удобную штуку сразу в двух крупных браузерах.

Функция progress() показывает, насколько некоторое значение продвинулось в диапазоне между начальной и конечной точками:

progress(<value>, <start>, <end>)

Если <value> меньше <start>, результат — 0. Если <value> достигает <end>, результат — 1. Любое промежуточное значение даёт десятичную дробь между 0 и 1.

Технически то же самое уже можно сделать с помощью вычисления в calc():

calc((value - start) / (end - start))

Но есть важное отличие! С progress() можно вычислять значения из смешанных типов данных (например, складывать px с rem), чего сейчас не умеет calc(). К примеру, мы можем получить значение прогресса в единицах вьюпорта из числового диапазона в пикселях:

progress(100vw, 400px, 1000px);

…и функция вернёт 0, когда ширина вьюпорта равна 400px, а по мере роста экрана до 1000px значение будет продвигаться к 1. То есть progress() может приводить разные единицы измерения к числу, и, как следствие, мы можем анимировать свойства вроде opacity (которое принимает число или процент) на основе вьюпорта (который — длина/расстояние).

Есть и другой обходной путь — с использованием функций tan() и atan2(). Я раньше так делал(а), чтобы создавать плавные переходы, зависящие от размеров вьюпорта. Но progress() сильно упрощает работу и делает её гораздо более поддерживаемой.

В подтверждение: мы можем оркестровать несколько анимаций по мере изменения размера экрана. В следующей демке я взял(а) пример из статьи про tan() и atan2(), но заменил(а) их на progress(). Работает как часы!

Довольно дерзкий пример. Что-то практичнее — снижать непрозрачность изображения по мере уменьшения экрана:

img {
  opacity: clamp(0.25, progress(100vw, 400px, 1000px), 1);
}

Попробуйте изменить размер окна в демо, чтобы увидеть, как меняется прозрачность изображения (при условии, что вы в Safari 26 или последнем Chrome).

Я «зажал» progress() с помощью clamp() в диапазоне от 0.25 до 1. Но по умолчанию progress() уже ограничивает значение между 0 и 1. Согласно релиз-нотсам WebKit, текущая реализация по умолчанию не клэмпится, однако в тестах это выглядит как клэмп. Так что если вы задаётесь вопросом, почему я ограничиваю то, что якобы уже ограничено, — вот почему.

В будущем, возможно, появится и неограниченная версия.

Самовыравнивание при абсолютном позиционировании

И вот ещё что! Теперь можно использовать align-self и justify-self внутри элементов с абсолютным позиционированием. Это не такая громкая фича, как другие, о которых мы говорили, но у неё есть удобный сценарий.

Например, иногда хочется разместить абсолютно позиционированный элемент точно по центру вьюпорта, но свойства семейства inset (то есть top, right, bottom, left и т. п.) считаются относительно верхнего левого угла элемента. Значит, чего-то такого, как ниже, будет недостаточно, чтобы идеально выровнять по центру, как мы ожидаем:

.absolutely-positioned {
  position: absolute;
  top: 50%;
  left: 50%;
}

Отсюда можно было бы сдвинуть элемент на половину его размеров через translate, чтобы добиться идеального центрирования. Но теперь у нас есть ключевое слово center, поддерживаемое в align-self и justify-self, — меньше «подвижных частей» в коде:

.absolutely-positioned {
  position: absolute;
  justify-self: center;
}

Любопытно, что align-self: center как будто центрирует элемент не относительно вьюпорта, а относительно самого себя. Я выяснил(а), что можно использовать значение anchor-center, чтобы центрировать элемент относительно его дефолтного якоря — в этом примере таким является вьюпорт:

.absolutely-positioned {
  position: absolute;
  align-self: anchor-center;
  justify-self: center;
}

И, конечно, place-self — это шорткат для свойств align-self и justify-self, так что ради краткости их можно объединить:

.absolutely-positioned {
  position: absolute;
  place-self: anchor-center center;
}

Что нового (для веба)?

Safari 26 — это не только «догоняющий» релиз относительно Chrome. Здесь полно действительно свежих вещей, которые мы получаем впервые, или доработок по мотивам реализаций в других браузерах. Давайте посмотрим на эти возможности.

Функция constrast-color()

constrast-color() сама по себе не новинка. Она появилась в Safari Technology Preview ещё в 2021 году и изначально называлась color-contrast(). В Safari 26 мы получили обновлённое имя, а вместе с ним — полировку поведения.

Задав определённое значение цвета, contrast-color() возвращает либо белый, либо чёрный — в зависимости от того, какой даст более резкий контраст с этим цветом. То есть, если мы задаём coral как цвет фона, можно поручить браузеру решить, будет ли цвет текста белым или чёрным для лучшего контраста с фоном:

h1 {
  --bg-color: coral;
  background-color: var(--bg-color);
  color: contrast-color(var(--bg-color));
}

Наш коллега Daniel Schwarz недавно разбирал функцию contrast-color() и выяснил, что на деле она не так уж хорошо вычисляет лучший контраст между цветами:

Без сомнений, главный недостаток в том, что contrast-color() возвращает только чёрный или белый. Если вам нужен не чёрный и не белый — ну… это печально.

Печально ещё и потому, что бывают случаи, когда ни белый, ни чёрный не дают достаточного контраста с заданным цветом, чтобы соответствовать рекомендациям WCAG по контрастности. Есть намерение расширить contrast-color(), чтобы она могла возвращать и другие значения цвета, но даже тогда останутся вопросы: как именно contrast-color() выбирает «лучший» цвет, ведь нам всё равно нужно учитывать насыщенность/ширину начертания, размер шрифта и даже гарнитуру. Всегда проверяйте реальный контраст!

И всё же, здорово, что contrast-color() наконец-то появилась — надеюсь, в будущем она станет лучше.

Красивые переносы текста

Safari 26 также добавляет text-wrap: pretty, и это, как ни странно, довольно «красиво» просто: абзацы переносятся визуально приятнее.

Помните, Chrome включил это ещё в 2023-м. Но обратите внимание на существенную разницу реализаций. В Chrome механизм лишь избегает типографских «сирот» (очень коротких последних строк). Safari идёт дальше и сильнее «приукрашает» переносы:

  • Предотвращает короткие строки. Избегает одиночных слов в конце абзаца.

  • Улучшает «рваный край»: длина строк становится более равномерной.

  • Сокращает количество переносов. Когда переносы включены, они улучшают ровность правого края, но при этом «ломают» слова. В целом переносов должно быть по минимуму.

В блоге WebKit это разбирают намного детальнее — если интересно, загляните, какие именно факторы они учитывали.

 Safari предпринимает дополнительные шаги, чтобы обеспечить «красивые» переносы текста, в том числе улучшая общий рисунок «рваного» края.
Safari предпринимает дополнительные шаги, чтобы обеспечить «красивые» переносы текста, в том числе улучшая общий рисунок «рваного» края.

Это только начало!

Думаю, это все CSS-новинки Safari, на которые стоит обратить внимание, но не хочу, чтобы казалось, будто в релизе больше ничего нет. Как я писал(а) вначале, речь о 75 новых возможностях Web Platform, включая HDR-изображения, поддержку SVG-иконок, логические свойства для overflow, margin-trim и многое другое. Определённо стоит просмотреть полные релиз-ноты.

Русскоязычное JavaScript сообщество

Друзья! Эту статью перевела команда «JavaScript for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!

Комментарии (1)


  1. proneta
    25.11.2025 08:37

    Наверное автор сильно торопился с переводом. Важные обстоятельства.

    " я взял(а) пример из статьи про tan()"

    Сиди и думай, на Хабре же не может быть копи-пастов. Наверное , это угол Альфа ).

    Но по форматированию вопросов не имею. Статья интересная.