Перейти до основного вмісту

Невидимий 11-піксельний семплер для Safari 26

Як я розібрав механізм тонування панелей Safari 26 у коді WebKit і сховав надійний семплер теми через background-clip: text.

SafariWebKitCSS

Сторінка вже була темною. Safari залишався світлим.

Напівпрозорі елементи браузера не підхоплювали тему сайту, а після закриття змісту чи перемикання теми знову спалахували блідим кольором. Сама сторінка виглядала правильно. Усе ламалося на межі між нею та елементами Safari.

Спочатку я шукав проблему в theme-color. Потім задавав колір для документа, body, фіксованої навігації й окремої смуги заввишки 11 пікселів угорі в’юпорту. Смуга нарешті давала Safari потрібний колір, але водночас ставала помітною суцільною частиною сторінки. Якщо повністю винести її за екран, тонування зникало. Прозорість теж не давала стабільного результату.

Перемикання тем, зміст, прокручування й повороти екрана пережив лише дивніший варіант: порожній фіксований елемент зі справжнім кольором фону, обрізаним за текстом, якого в елементі немає.

Сім тижнів заради блока заввишки 11 пікселів

Ця історія почалася не 25 липня. Уперше я розбирав тонування Safari 26 6 червня в pull request 7 (opens in a new tab) (відкриється в новій вкладці). Тоді вдалося визначити фіксовану навігацію як джерело верхнього кольору Safari, а зону статусу заповнила суцільна смуга заввишки 44 пікселі. 10 червня я дослідним шляхом звузив мінімум до 11 пікселів. Здавалося, проблему нарешті вдалося втримати.

Мобільний зміст довів протилежне. Після його закриття Safari міг утратити тонування, а 11-піксельна смуга все одно залишалася справжнім намальованим елементом. 25 липня я почав останню сесію налагодження з відтворення помилки о 16:26. Остаточний знімок у Simulator з’явився о 21:21, майже через п’ять годин.

Сторінка Блогу з відповідним кольором елементів Safari у світлій і темній темах в iPhone 17 Pro Simulator о 21:21Сторінка Блогу з відповідним кольором елементів Safari у світлій і темній темах в iPhone 17 Pro Simulator о 21:21
21:21, 25 липня: остаточна перевірка обох тем у Simulator, де сторінка й елементи Safari знову належать до однієї візуальної сцени. · iPhone 17 Pro Simulator, iOS 26.5

На той момент ця помилка вже добряче мене дістала. Кожен варіант міг виглядати виправленим, доки перемикання теми, закриття змісту чи поворот екрана не виявляли чергове слабке припущення. Найбільше дратувала суцільна смуга. Safari отримував потрібний колір, а читачі бачили елемент, якого на сторінці взагалі не мало бути.

Від першого записаного дослідження до невидимого семплера минуло рівно сім тижнів.

Проблема була ширшою за theme-color

Стандарт HTML описує theme-color як рекомендований колір (opens in a new tab) (відкриється в новій вкладці) для інтерфейсу навколо сторінки. Ці метадані досі корисні, тому я залишив їх як резервний варіант. Проте в моїх перевірках Safari 26 цього було недостатньо для Liquid Glass навколо фіксованої поверхні сторінки.

Загальний принцип такої поведінки навмисний. У звіті WebKit 301756 (opens in a new tab) (відкриється в новій вкладці) інженер WebKit Wenson Hsieh пояснює, що Safari продовжує суцільний колір біля прихованого краю в’юпорту, коли до нього прив’язаний елемент із fixed або sticky. Інакше під час прокручування між поверхнею сторінки й елементами Safari могла б з’явитися щілина. На iPhone вона особливо помітна через м’якше розмиття.

Тому накладки й робили помилку схожою на випадковість. Висувна панель, діалог або popover можуть додати нову фіксовану поверхню над навігацією. У звіті 302272 (opens in a new tab) (відкриється в новій вкладці) описано, як після появи фіксованої накладки верхнє й нижнє тонування підхоплює її фон. WebKit позначив цей звіт як дублікат звіту 300965 (opens in a new tab) (відкриється в новій вкладці) про фон модального діалогу. У виправленні та його тестах (opens in a new tab) (відкриється в новій вкладці) краї фіксованого контейнера зчитуються до відкриття діалогу, під час його показу й після закриття.

Safari не просто читав один метатег. Він визначав, яка поверхня сторінки володіє краєм в’юпорту.

Варіанти, які майже спрацювали

Невдалі спроби виявилися корисними, бо кожна прибирала одне хибне припущення.

СпробаРезультат
Оновлювати theme-color, html і bodyПравильний колір під час завантаження, але він втрачав пріоритет, коли Safari вибирав фіксовану поверхню на краю
Використати видиму навігаціюПрацювало, доки висувна панель чи інший фіксований шар не опинялися вище
Додати кольорову смугу заввишки 44, а потім 11 пікселівСтабільне тонування, але видима суцільна смуга вгорі
Пересунути смугу до top: -9pxНа сторінці залишалися два зафарбовані пікселі
Винести всі 11 пікселів за екранSafari теж переставав бачити елемент на краю
Застосувати opacity: 0 або прозоре малюванняЕлемент або втрачав право на зчитування, або не замінював попередній колір
Монтувати й демонтувати допоміжний елемент разом зі змістомЗ’являвся крихкий стан життєвого циклу, який не охоплював усі перерахунки

Найбільше допомогли два польові дослідження: експерименти Джахіра Фікітіви з панелями Safari (opens in a new tab) (відкриється в новій вкладці) і нотатки Павла Ларіонова про Safari 26 (opens in a new tab) (відкриється в новій вкладці). Обидва автори незалежно пов’язали колір із геометрією фіксованого елемента на краю та порогом близько 11 пікселів. Проте деталі відрізняються між версіями Safari. Наприклад, актуальний код WebKit відкидає елементи з нульовою непрозорістю, тому результат з opacity: 0 у старішій збірці я не вважав стабільним контрактом.

Після цих перевірок у мене залишилося одне питання: чи може WebKit побачити непрозорий колір фону, коли сторінка не малює жодного видимого пікселя?

Simulator зробив цей цикл терпимим

Я не розгортав кожну CSS-гіпотезу на Vercel, щоб потім відкривати її на телефоні. Safari в Simulator на macOS завантажував localhost:3000, а Safari Web Inspector під’єднувався саме до цієї сторінки. Можна було змінити CSS, оновити сторінку, перевірити обчислені стилі, перемкнути тему, відкрити й закрити зміст та змінити орієнтацію екрана, не випадаючи з розробки.

Safari Web Inspector у світлому й темному оформленні, під’єднаний до localhost в iPhone 17 Pro Simulator, з DOM сторінки та обчисленими стилямиSafari Web Inspector у світлому й темному оформленні, під’єднаний до localhost в iPhone 17 Pro Simulator, з DOM сторінки та обчисленими стилями
Safari Web Inspector, під’єднаний безпосередньо до сторінки з localhost у Simulator, у світлому й темному оформленні. · Safari Web Inspector у macOS

Simulator не замінив справжній iPhone. Він швидко відсіював погані ідеї. Коли версія проходила всю локальну матрицю, я надсилав попередню збірку й робив остаточну перевірку на справжньому пристрої. Такий цикл значно швидший за послідовні розгортання, перевірки й повторні розгортання.

Що WebKit перевіряє на краю

Відповідь знайшлася у LocalFrameView::fixedContainerEdges. Я простежив реалізацію в зафіксованій ревізії WebKit, а не намагався вгадати її за скриншотами.

Для кожного прихованого краю в’юпорту WebKit створює вузьку ділянку перевірки, трохи стискає її та виконує hit test біля середини. Далі він аналізує найвищий у стеку елемент, прив’язаний до в’юпорту й цього краю. Саме тому важливі порядок у підготовці краю (opens in a new tab) (відкриється в новій вкладці) та шляху hit test (opens in a new tab) (відкриється в новій вкладці).

Класифікатор кандидата перевіряє, що елемент:

  • має fixed або sticky та прилягає до краю в’юпорту;
  • не прихований і не повністю прозорий;
  • для горизонтального краю перекриває щонайменше 90 відсотків ширини в’юпорту;
  • має придатний і достатньо суцільний колір фону.

Значення 11 пікселів походить не з базової придатності до hit test, а зі швидкого шляху для обчисленого кольору. primaryBackgroundColorForRenderer відмовляється від нього, якщо хоча б один розмір рамки не перевищує 10 пікселів. Тоді WebKit може спробувати прочитати намальовані пікселі або попередній колір краю, а це погано підходить для навмисно порожнього малювання. 10-піксельний поріг (opens in a new tab) (відкриється в новій вкладці) і перевірки кандидата (opens in a new tab) (відкриється в новій вкладці) пояснюють результат спроб. 11 пікселів не є safe-area-inset чи дизайнерським відступом. Це перша ціла висота в CSS понад поріг.

Важливим виявився спосіб отримання кольору. Механізм для краю читає обчислений background-color елемента. Звичайне малювання сторінки застосовує обрізання фону пізніше, в іншій частині коду.

Зчитування кольору фіксованого краю в Safari

WebKit знаходить найвищий фіксований елемент на краю та читає його обчислений колір фону, а звичайний малювальник обрізає фон за порожнім текстом і не створює видимих пікселів.

Використовуйте плюс і мінус для масштабування, стрілки для навігації збільшеною діаграмою, а Home — для скидання.

Тож семплер може віддати Safari колір одним шляхом, а власне малювання виконати іншим.

Обрізання за порожнім текстом

Семплер є порожнім div, змонтованим перед видимою навігацією:

Header.tsx
<div
  aria-hidden="true"
  className="safari-chrome-sample"
  data-safari-chrome-sample
/>

Для мобільного WebKit він має такі стилі:

globals.css
.safari-chrome-sample {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  z-index: 2147483647;
  height: 0;
  border: 0;
  outline: 0;
  pointer-events: none;
  background-color: var(--safari-chrome-color);
  background-clip: text;
  -webkit-background-clip: text;
  box-shadow: none;
  transition: none;
}
 
@supports (-webkit-touch-callout: none) {
  @media (hover: none) and (pointer: coarse) {
    .safari-chrome-sample {
      height: 11px;
    }
 
    html[data-chrome-sample-refresh] .safari-chrome-sample {
      height: 12px;
    }
  }
}

background-clip: text не робить обчислений колір фону прозорим. Властивість лише визначає, де цей фон буде намальовано. Усередині елемента немає тексту, тому немає і гліфів для маски. Елемент зберігає повну рамку заввишки 11 пікселів і непрозорий обчислений колір, але не додає на сторінку жодного видимого кольорового пікселя.

У коді малювальника фону WebKit (opens in a new tab) (відкриється в новій вкладці) обрізання за текстом відбувається окремо від перевірки придатності фіксованого краю. Рішення використовує саме цей проміжок між двома механізмами.

Великий z-index не робить елемент невидимим. Він гарантує, що порожній семплер виграє єдиний hit test найвищого елемента, навіть коли відкрито висувну панель чи діалог. pointer-events: none прибирає його зі взаємодії, а aria-hidden і порожній DOM-вузол не додають нічого до доступного інтерфейсу.

Поза WebKit із грубим сенсорним вказівником висота залишається нульовою. Тому макети для настільних браузерів і адаптивний режим Chrome не змінюються. Інші браузери можуть розібрати базове правило, але не отримують ні намальованого вмісту, ні мобільного блока семплера.

Навіщо досі потрібна зміна з 11 до 12 пікселів

Після зміни CSS-змінної обчислений колір семплера оновлювався, але Safari 26 не завжди перераховував кешований фіксований край. Найлегше це було відтворити після закриття змісту або прямого перемикання між світлою й темною темами.

Тому під час застосування теми початковий скрипт на один намальований кадр змінює геометрію кандидата:

no-flash-script.ts
function refreshSafariChromeSample() {
  window.cancelAnimationFrame(sampleRefreshFrame);
  element.dataset.chromeSampleRefresh = "";
 
  sampleRefreshFrame = window.requestAnimationFrame(() => {
    sampleRefreshFrame = window.requestAnimationFrame(() => {
      delete element.dataset.chromeSampleRefresh;
      sampleRefreshFrame = 0;
    });
  });
}

Атрибут даних збільшує висоту невидимого блока з 11 до 12 пікселів. Два анімаційні кадри дають Safari показати змінену геометрію, перш ніж повернутися до 11 пікселів. В обох станах блок нічого не малює.

Я запускаю це оновлення після застосування теми, під час системної зміни оформлення, після синхронізації сховища між вкладками й у події pageshow. Скрипт проти спалаху також задає визначений колір кореневому елементу ще до розбору body і фіксованої навігації. Завдяки цьому документ не починає зі світлого кольору, якщо збережена темна тема.

CSS семплера (opens in a new tab) (відкриється в новій вкладці) та початковий скрипт теми (opens in a new tab) (відкриється в новій вкладці) прив’язані до коміту, який я перевірив на справжньому пристрої.

Що я перевірив

Остаточна перевірка охопила переходи, на яких ламалися попередні варіанти:

  • початкове завантаження у світлій і темній темах;
  • перемикання зі світлої теми на темну та навпаки;
  • відкриття й закриття мобільного змісту в обох темах;
  • ті самі дії після прокручування далеко вглиб статті;
  • обидва напрямки альбомної орієнтації та повернення до портретної;
  • відновлення сторінки через pageshow;
  • перевірку настільного Chrome та його адаптивного режиму на регресії макета;
  • попередню збірку на справжньому iPhone.

Це не перетворює внутрішню евристику WebKit на публічний API браузера. Safari може змінити геометрію перевірки, порогові значення чи порядок малювання. Тому семплер має кілька навмисних правил підтримки: залишати елемент порожнім, тримати його розміри вище поточного порога, не додавати рамок, тіней, псевдовмісту чи переходів і зберігати theme-color як стандартний резервний варіант.

Я досі називаю це хаком. Він спирається на приватну евристику браузера та розрив між перевіркою стилю й малюванням. Проте користувачі його не бачать, Safari тепер витримує всі стани, які ламали попередні варіанти, а тонування більше не руйнує цілісність сторінки. Після семи тижнів історії та майже п’яти годин в останній сесії я задоволений результатом.

Джерела

affb7eb