Эта страница переведена с английского. Читать страницу на английском
Статус: реализовано в 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 | Код, примеры API | ui-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-bg | 15.49 | 14.77 | 4.5 |
--z-text на --z-surface | 16.91 | 13.54 | 4.5 |
--z-text на --z-surface-2 | 15.91 | 12.63 | 4.5 |
--z-text на --z-hover | 14.55 | 10.93 | 4.5 |
--z-text на --z-brand-subtle (строка в фокусе) | 14.79 | 9.27 | 4.5 |
--z-text на --z-danger-subtle | 14.73 | 9.76 | 4.5 |
--z-text на --z-warning-subtle | 14.82 | 9.31 | 4.5 |
--z-text на --z-success-subtle | 14.76 | 9.33 | 4.5 |
--z-text-muted на --z-bg | 5.65 | 7.78 | 4.5 |
--z-text-muted на --z-surface | 6.17 | 7.13 | 4.5 |
--z-text-muted на --z-surface-2 (заголовки) | 5.80 | 6.65 | 4.5 |
--z-text-muted на --z-hover | 5.31 | 5.76 | 4.5 |
--z-text-muted на --z-danger-subtle (приглушённая строка) | 5.37 | 5.14 | 4.5 |
--z-text-muted на --z-warning-subtle | 5.41 | 4.90 | 4.5 |
--z-accent на --z-surface (ссылки) | 6.62 | 9.33 | 4.5 |
--z-accent на --z-bg | 6.06 | 10.18 | 4.5 |
--z-accent на --z-brand-subtle (текущий пункт меню) | 5.79 | 6.39 | 4.5 |
--z-on-primary на --z-primary | 6.62 | 10.18 | 4.5 |
--z-on-primary на --z-primary-hover | 8.82 | 11.26 | 4.5 |
--z-on-primary на --z-primary-active | 11.12 | 12.26 | 4.5 |
--z-on-brand на --z-brand (полоса с названием приложения) | 6.62 | 6.62 | 4.5 |
--z-danger на --z-surface (ошибки полей) | 6.54 | 7.09 | 4.5 |
--z-danger на --z-danger-subtle | 5.69 | 5.11 | 4.5 |
--z-warning на --z-surface | 6.33 | 8.90 | 4.5 |
--z-warning на --z-warning-subtle | 5.55 | 6.12 | 4.5 |
--z-success на --z-surface | 6.52 | 8.87 | 4.5 |
--z-info на --z-surface | 6.92 | 8.13 | 4.5 |
--z-focus на --z-surface | 6.62 | 9.33 | 3 |
--z-focus на --z-bg | 6.06 | 10.18 | 3 |
--z-focus на --z-brand-subtle | 5.79 | 6.39 | 3 |
--z-border-strong на --z-surface (контур поля ввода) | 3.78 | 4.30 | 3 |
--z-border-strong на --z-surface-2 | 3.55 | 4.01 | 3 |
--z-border декоративен (разделители между элементами, у которых уже есть текст) и к порогу 3:1 не приравнивается.
6. Прочее поведение для доступности
- Каждый интерактивный элемент показывает кольцо
:focus-visibleтолщиной 2 px цвета--z-focus. Строки сетки рисуют кольцо внутри строки, поэтому контейнер прокрутки его никогда не обрезает. - Кнопки, поля ввода, заголовки сортировки, поля фильтров, кнопки пейджера и кнопка закрытия окна имеют размер не менее 24 x 24 px при любой плотности.
prefers-reduced-motion: reduceубирает появление уведомлений и все переходы.- В режиме
forced-colorsзначки используютCanvasText, основная кнопка и строка в фокусе используютHighlight, а состояния, опирающиеся на фоновые оттенки (текущий пункт меню, статус строки), получают контур или рамку.