Перейти к содержимому

Темы оформления

Эта страница переведена с английского. Читать страницу на английском

Статус: реализовано в crates/zeyn/assets/zeyn.css.

Стандартный набор UI берёт каждый цвет, шрифт, размер и радиус из CSS custom properties в :root. Организация меняет внешний вид, переопределяя несколько из них. Править zeyn.css или писать селекторы не нужно.

1. Как применяется тема

Сервер сначала отдаёт zeyn.css, а затем, если у приложения он есть, дополнительную таблицу стилей с переопределениями токенов. Файл переопределений содержит только блоки :root:

:root {
  --z-brand: #7a2448;
  --z-font: "Source Sans 3", "Segoe UI", system-ui, sans-serif;
  --z-radius: 8px;
  --z-density: 0.75;
}

Правила для файла переопределений:

  • Он должен отдаваться с собственного origin приложения. Content Security Policy задана как style-src 'self', поэтому встроенные блоки <style> и сторонние таблицы стилей блокируются.
  • Шрифты должны быть уже установлены на машинах пользователей или отдаваться с origin приложения. Сам набор не загружает веб-шрифты.
  • Подключайте файл после zeyn.css, чтобы при равных селекторах побеждал порядок.
  • Задавайте базовые токены из раздела 2. Производные токены из раздела 3 следуют за ними автоматически. Переопределяйте производный токен только тогда, когда вычисленное значение не подходит вашему бренду.

Светлая, тёмная и тематики приложения

В каждом приложении в меню, рядом с выбором языка, есть выбор темы. Он предлагает System (следует prefers-color-scheme), Light, Dark и любые темы, которые определило приложение:

App::new("Billing")
    .theme(Theme::new().brand("#7a2448").radius(6))
    .themes([
        Theme::light(),
        Theme::dark(),
        Theme::named("sand", [("--z-bg", "#f5f1ea"), ("--z-surface", "#fffdf8")]).label(t!("theme.sand")),
    ])
    .default_theme("system")
  • App::theme задаёт внешний вид организации и действует во всех темах.
  • App::themes перечисляет то, что могут выбрать пользователи. Без неё список состоит из Light и Dark. System предлагается, когда в списке есть обе. Theme::named(...) строится на светлой теме; .dark_base() строит её на тёмной.
  • App::default_theme задаёт тему для пользователей, которые ничего не выбрали. Если не задана, используется system.
  • App::theme_picker(false) скрывает выбор темы; все получают тему по умолчанию.
  • Одна тема фиксирует приложение: .themes([Theme::named("acme", tokens)]) или .theme(Theme::dark()).

Токены проверяются при запуске приложения. App::check (а значит, router и serve) сообщает об неизвестных именах токенов, о значениях, которые могут выйти за пределы своего объявления (;, фигурные скобки, \, <, >, url(), о некорректных именах тем, об неизвестной теме по умолчанию и о каждой цветовой паре из раздела 5, которую тема опускает ниже WCAG 2.2 AA. Контраст можно проверить только для значений #rgb и #rrggbb; другой синтаксис цвета принимается на доверии.

Все темы отдаются одной таблицей стилей, /_zeyn/theme.css, которая строится один раз при запуске и подключается после стилей набора. Выбор хранится в cookie zeyn_theme и, для вошедшего пользователя в приложении с хранилищем представлений (App::datagrid_views), в этом хранилище, поэтому он следует за пользователем на новое устройство. Сервер выводит тему в <html>, поэтому первая отрисовка уже верна и встроенный скрипт не нужен:

РазметкаРезультат
<html>System: следует prefers-color-scheme
<html data-theme="light">Всегда светлая
<html data-theme="dark">Всегда тёмная
<html data-theme="light" data-zeyn-theme="sand">Тема приложения sand на основе светлой

Смена темы в выборе меняет только эти атрибуты и meta color-scheme, а затем вызывает POST /_zeyn/theme. Ничего не перезагружается.

Чтобы задать тёмной теме собственные значения в файле переопределений, написанном вручную, повторите селекторы, которые использует набор:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --z-bg: #0e1012; }
}
:root[data-theme="dark"] { --z-bg: #0e1012; }

Плотность

--z-density масштабирует единицу отступа. 1 соответствует обычной плотности, 0.75 компактной. <html data-density="compact"> задаёт 0.75 без таблицы стилей. Элементы управления и строки сетки никогда не становятся меньше 24 px, поэтому компактный режим по-прежнему соответствует размеру цели WCAG 2.2.

Высота строки виртуальной сетки задаётся сервером (Paging::virtual_rows(px), по умолчанию 32 px), потому что рантайм вычисляет по ней позиции прокрутки. Плотность её не меняет.

2. Базовые токены

Это токены, которые организация, как ожидается, задаёт сама.

ТокенНазначениеСветлаяТёмная
--z-brandЕдинственный цвет бренда. Основные кнопки, полоса с названием приложения, текущий пункт меню, строка в фокусе, кольцо фокуса#0f6674то же значение; см. --z-accent
--z-on-brandТекст на сплошной поверхности --z-brand#ffffff#ffffff
--z-fontСтек шрифтов интерфейса"Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, "Noto Sans", sans-serifто же
--z-font-monoКод, примеры APIui-monospace, "Cascadia Mono", "SF Mono", "Segoe UI Mono", Menlo, Consolas, monospaceто же
--z-font-sizeОсновной текст0.875rem (14 px)то же
--z-font-size-smЯчейки сетки, подписи, фильтры0.8125rem (13 px)то же
--z-font-size-xsЗаголовки столбцов, названия групп меню, сведения о странице0.75rem (12 px)то же
--z-radiusБазовый радиус скругления, от 0 до 12 px. От него масштабируется каждый компонент4pxто же
--z-densityМножитель отступов, 1 обычная, 0.75 компактная1то же
--z-bgФон страницы за панелями#f4f5f6#121416
--z-surfaceПанели, сетки, окна, поля ввода#ffffff#1a1d20
--z-surface-2Заголовок сетки, полосатые строки, нижняя часть окна, поля только для чтения#f7f8f9#202326
--z-hoverФон при наведении для строк, пунктов меню, кнопок#eceef1#2a2e33
--z-borderРазделители и контуры панелей#dcdfe3#33383e
--z-border-strongКонтуры полей ввода и кнопок, линия заголовка. Должен достигать 3:1 относительно --z-surface#7d848d#7a818a
--z-textОсновной текст и значения#1a1d21#e4e6e9
--z-text-mutedПодписи, заголовки, второстепенный текст. Должен достигать 4.5:1 относительно --z-surface-2#5b626b#a2a9b1
--z-dangerОшибки, отметки обязательности, строки с опасностью#b3261e#f28b82
--z-warningСтроки с предупреждением#8a5300#e6b45a
--z-successСтроки с успехом#1e6b3a#6fcf8f
--z-infoИнформационные уведомления#2b5d8a#8ab8e6

3. Производные токены

Вычисляются из базовых токенов с помощью color-mix() и синтаксиса относительных цветов. Обновляются сами при изменении базового токена.

ТокенНазначениеСветлаяТёмная
--z-accentЦвет бренда, используемый как текст или тонкая линия на поверхности: ссылки, текущий пункт меню, полоса строки в фокусеvar(--z-brand)oklch(from var(--z-brand) 0.8 c h), оттенок бренда с фиксированной светлотой. Браузеры без синтаксиса относительных цветов получают color-mix(in oklab, var(--z-brand) 45%, #fff)
--z-primaryЗаливка основной кнопкиvar(--z-brand)var(--z-accent)
--z-on-primaryТекст основной кнопкиvar(--z-on-brand)#121416
--z-primary-hoverОсновная кнопка при наведениибренд 86% с примесью чёрногоакцент 84% с примесью белого
--z-primary-activeОсновная кнопка в нажатом состояниибренд 74% с примесью чёрногоакцент 70% с примесью белого
--z-brand-subtleОттенок под текущим пунктом меню и строкой сетки в фокусеакцент при --z-tint поверх --z-surfaceта же формула
--z-focusКольцо фокусаvar(--z-accent)var(--z-accent)
--z-danger-subtle, --z-warning-subtle, --z-success-subtle, --z-info-subtleФоны строк и сообщенийцвет статуса при --z-tint поверх --z-surfaceта же формула
--z-tintСила приглушённых оттенков9%20%
--z-spaceЕдиница отступаcalc(4px * var(--z-density))то же
--z-controlВысота кнопки и поля вводаmax(24px, 8 * --z-space) (32 px)то же
--z-rowВысота строки сетки для постраничных и бесконечных сетокmax(24px, 8 * --z-space) (32 px)то же
--z-gapПромежуток между секциями экрана3 * --z-space (12 px)то же
--z-radius-sm, --z-radius-lgМелкие части (фильтры, код), окна0.75x и 1.5x от --z-radiusто же
--z-shadow-2Только окна и уведомления. У строк сетки тени нетмягкая двухслойная теньтемнее
--z-backdropФон за модальным окномrgb(20 24 28 / 40%)rgb(0 0 0 / 60%)
--z-dirНаправление чтения в виде знака, для смещений без логической формы (внутренние тени, отмечающие начальную или конечную сторону, сдвиги): calc(2px * var(--z-dir))1, и -1 при dir="rtl"то же

Токены отдельных элементов, которые задаёт серверная разметка: --z-width (ширина окна), --z-flex (доля столбца), --z-row-height (высота строки виртуальной сетки), а для библиотеки компонентов --z-stack-gap, --z-split, --z-progress, --z-columns, --z-level и --z-pop-x / --z-pop-y (позиция всплывающего окна).

Цвета рядов диаграмм это --z-chart-1 ... --z-chart-6 в components.css, в тёмной теме с более светлыми значениями. Каждый достигает 3:1 относительно --z-surface. Переопределяйте их как любой базовый токен.

4. Выбор цвета бренда

  • --z-brand несёт белый текст в светлой теме (кнопки, полоса с названием приложения). Выбирайте цвет с контрастом не менее 4.5:1 относительно --z-on-brand. Большинство средних и тёмных цветов бренда проходят. Для светлого цвета бренда, например жёлтого, задайте также тёмный --z-on-brand.
  • В тёмной теме набор осветляет бренд для текста, колец и основных кнопок, а сплошной бренд оставляет для полосы с названием приложения. Другой бренд выглядит продуманно в обеих темах без второго цвета.
  • Держите цвета статусов отличимыми от бренда. Красный бренд рядом с красными строками ошибок затрудняет их заметность; в этом случае сдвиньте --z-danger к другому оттенку красного или уберите бренд из сеток, переопределив --z-brand-subtle.
  • Проверяйте свои цвета, ориентируясь на таблицу из раздела 5. Любая пара ниже порога не проходит WCAG 2.2 AA.

5. Проверенный контраст

Измерено в Chromium на работающем примере приложения: каждый токен был приведён к отрисованному значению sRGB (включая color-mix() и относительные цвета), затем вычислено отношение контраста WCAG 2.x. Пороги: 4.5:1 для текста, 3:1 для контуров компонентов UI и индикаторов фокуса.

ПараСветлаяТёмнаяНужно
--z-text на --z-bg15.4914.774.5
--z-text на --z-surface16.9113.544.5
--z-text на --z-surface-215.9112.634.5
--z-text на --z-hover14.5510.934.5
--z-text на --z-brand-subtle (строка в фокусе)14.799.274.5
--z-text на --z-danger-subtle14.739.764.5
--z-text на --z-warning-subtle14.829.314.5
--z-text на --z-success-subtle14.769.334.5
--z-text-muted на --z-bg5.657.784.5
--z-text-muted на --z-surface6.177.134.5
--z-text-muted на --z-surface-2 (заголовки)5.806.654.5
--z-text-muted на --z-hover5.315.764.5
--z-text-muted на --z-danger-subtle (приглушённая строка)5.375.144.5
--z-text-muted на --z-warning-subtle5.414.904.5
--z-accent на --z-surface (ссылки)6.629.334.5
--z-accent на --z-bg6.0610.184.5
--z-accent на --z-brand-subtle (текущий пункт меню)5.796.394.5
--z-on-primary на --z-primary6.6210.184.5
--z-on-primary на --z-primary-hover8.8211.264.5
--z-on-primary на --z-primary-active11.1212.264.5
--z-on-brand на --z-brand (полоса с названием приложения)6.626.624.5
--z-danger на --z-surface (ошибки полей)6.547.094.5
--z-danger на --z-danger-subtle5.695.114.5
--z-warning на --z-surface6.338.904.5
--z-warning на --z-warning-subtle5.556.124.5
--z-success на --z-surface6.528.874.5
--z-info на --z-surface6.928.134.5
--z-focus на --z-surface6.629.333
--z-focus на --z-bg6.0610.183
--z-focus на --z-brand-subtle5.796.393
--z-border-strong на --z-surface (контур поля ввода)3.784.303
--z-border-strong на --z-surface-23.554.013

--z-border декоративен (разделители между элементами, у которых уже есть текст) и к порогу 3:1 не приравнивается.

6. Прочее поведение для доступности

  • Каждый интерактивный элемент показывает кольцо :focus-visible толщиной 2 px цвета --z-focus. Строки сетки рисуют кольцо внутри строки, поэтому контейнер прокрутки его никогда не обрезает.
  • Кнопки, поля ввода, заголовки сортировки, поля фильтров, кнопки пейджера и кнопка закрытия окна имеют размер не менее 24 x 24 px при любой плотности.
  • prefers-reduced-motion: reduce убирает появление уведомлений и все переходы.
  • В режиме forced-colors значки используют CanvasText, основная кнопка и строка в фокусе используют Highlight, а состояния, опирающиеся на фоновые оттенки (текущий пункт меню, статус строки), получают контур или рамку.