Сторінка вже була темною. 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 отримував потрібний колір, а читачі бачили елемент, якого на сторінці взагалі не мало бути.
Від першого записаного дослідження до невидимого семплера минуло рівно сім тижнів.
Проблема була ширшою за 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, оновити сторінку,
перевірити обчислені стилі, перемкнути тему, відкрити й закрити зміст та змінити
орієнтацію екрана, не випадаючи з розробки.


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 елемента. Звичайне малювання сторінки застосовує
обрізання фону пізніше, в іншій частині коду.
WebKit знаходить найвищий фіксований елемент на краю та читає його обчислений колір фону, а звичайний малювальник обрізає фон за порожнім текстом і не створює видимих пікселів.
Використовуйте плюс і мінус для масштабування, стрілки для навігації збільшеною діаграмою, а Home — для скидання.
Тож семплер може віддати Safari колір одним шляхом, а власне малювання виконати іншим.
Обрізання за порожнім текстом
Семплер є порожнім div, змонтованим перед видимою навігацією:
<div
aria-hidden="true"
className="safari-chrome-sample"
data-safari-chrome-sample
/>Для мобільного WebKit він має такі стилі:
.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 не завжди перераховував кешований фіксований край. Найлегше це було відтворити після закриття змісту або прямого перемикання між світлою й темною темами.
Тому під час застосування теми початковий скрипт на один намальований кадр змінює геометрію кандидата:
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 тепер витримує всі стани, які ламали попередні варіанти, а тонування більше не руйнує цілісність сторінки. Після семи тижнів історії та майже п’яти годин в останній сесії я задоволений результатом.
Джерела
- Звіт WebKit 301756: навіщо Safari продовжує кольори фіксованого краю (opens in a new tab) (відкриється в новій вкладці)
- Звіт WebKit 302272: зміна тонування після появи фіксованих накладок (opens in a new tab) (відкриється в новій вкладці)
- Звіт WebKit 300965: продовження кольору фону діалогу (opens in a new tab) (відкриється в новій вкладці)
- Коміт WebKit 2ae949b: зчитування фону фіксованого контейнера (opens in a new tab) (відкриється в новій вкладці)
-
Механізм фіксованого краю в
LocalFrameViewWebKit (opens in a new tab) (відкриється в новій вкладці) -
Обрізання за текстом у
BackgroundPainterWebKit (opens in a new tab) (відкриється в новій вкладці) -
Стандарт HTML:
theme-color(opens in a new tab) (відкриється в новій вкладці) - Джахір Фікітіва: експерименти з панелями Safari 26 (opens in a new tab) (відкриється в новій вкладці)
- Павло Ларіонов: нотатки про Liquid Glass у Safari 26 (opens in a new tab) (відкриється в новій вкладці)
- Реалізація та дослідження в pull request 34 (opens in a new tab) (відкриється в новій вкладці)