Как передать одностраничник в вёрстку через файл. Токены из стилей Figma, вариативный шрифт и прелоадер как спецификация

Я делал дизайн одностраничного сайта для новой марки куриных яиц. Страница длинная, больше одиннадцати тысяч пикселей на десктопе, с прелоадером, закреплёнными сценами и анимацией между блоками. Макет я сдавал в двух ширинах, 1600 и 390. Вёрстку и разработку вела другая команда, моя часть это дизайн и передача макета.
Слоёв в передаче пять. Цвет, вариативный шрифт, сетка, прелоадер и карточка упаковки. По каждому покажу, что лежало в Figma и как я бы выгрузил это разработчику сейчас, токенами и короткими спецификациями. Код в статье мой, это пример формата передачи, а не вёрстка сайта.

Упаковка, два требования и передача через файл
От бренда была упаковка с рукописной надписью и тремя цветами. Требований к сайту два. Говорить с городским покупателем, хотя вся категория построена на деревне, и добавлять новые упаковки без перерисовки страниц.
Переменных Figma в файле нет, система собрана на стилях. Стилей цвета четыре, текстовых пять, сеток две. Разработчики сидели в другой компании, общались мы через файл и переписку, значит, всё, чего нет в файле, им пришлось бы решать самим. И страница почти целиком анимирована, а статичный макет показывает только начало и конец каждого движения.
Я рассматривал два способа передачи. Первый это полная спецификация с токенами под CSS, таблицей таймингов и отдельной страницей состояний. Второй это стили и UI-кит в Figma плюс записанные на мокапах ролики анимации. Выбрал второй, потому что одностраничник, сроки и ролики показывают движение лучше таблиц. Сейчас я бы отдавал оба.
Цвет. Из стиля Figma в переменную
В макете четыре стиля цвета. Значения сняты через Plugin API, вместе с дробной частью каналов.
Стиль | Каналы RGB в Figma (0–255) | Hex после округления | Применений через стиль |
|---|---|---|---|
Main/1 | 23; 58; 41 | #173A29 | 3 719 |
Main/2 | 255; 249; 231 | #FFF9E7 | 945 |
Accent/1 | 255; 192,882; 6,527 | #FFC107 | 571 |
Accent/2 | 110,196; 176,232; 77,757 | #6EB04E | 128 |
Figma хранит цвет как три числа от 0 до 1. У двух стилей значения не попадают в целые байты, и hex зависит от того, округляешь ты или отбрасываешь дробную часть. 192,882 даёт C1 при округлении и C0 при отбрасывании. Я сам в первой описи макета записал жёлтый как #FFC006 и светло-зелёный как #6EB04D, потому что скрипт отбрасывал дробь. Интерфейс Figma показывает hex с округлением, поэтому в выгрузке правильным значением считаю округлённое, #FFC107. Если токены снимаются скриптом, округление нужно прописать в нём явно.
Четырёх стилей для страницы оказалось мало. В UI-ките я нарисовал кнопки в четырёх состояниях, и у наведения свои цвета, #FFD000 у жёлтой кнопки и #194630 у тёмной. Стилей у них нет, они живут только на слоях кита. Розовый #FA73BF встречается в макете 64 раза и ни разу через стиль, я считал его частью иллюстраций. Полупрозрачный бежевый и полупрозрачный зелёный, 50% от основных цветов, тоже заданы прямо на слоях. Всё это разработчику пришлось бы снимать с картинок.
С бежевым в макете путаница. Через стиль Main/2 он применён 945 раз, а тем же значением без стиля ещё 2 608 раз, часть из них в копиях кадров для превью анимации. По такому файлу нельзя понять, какие бежевые слои принадлежат системе, а какие случайно совпали по цвету.
Цвет я бы отдавал так. Примитивы называются по стилям Figma, состояния и прозрачности получают свои имена, а роли ссылаются на примитивы.
/* пример выгрузки, имена по стилям Figma */
:root {
/* примитивы */
--main-1: #173A29; /* Main/1, тёмно-зелёный */
--main-2: #FFF9E7; /* Main/2, бежевый */
--accent-1: #FFC107; /* Accent/1, жёлтый */
--accent-2: #6EB04E; /* Accent/2, светло-зелёный */
--accent-1-hover: #FFD000; /* наведение жёлтой кнопки, в Figma без стиля */
--main-1-hover: #194630; /* наведение тёмной кнопки, в Figma без стиля */
--illustration-pink: #FA73BF;
/* роли */
--color-bg: var(--main-2);
--color-text: var(--main-1);
--color-text-muted: color-mix(in srgb, var(--main-1) 50%, transparent);
--color-on-dark-muted: color-mix(in srgb, var(--main-2) 50%, transparent);
--color-button: var(--accent-1);
--color-button-hover: var(--accent-1-hover);
}Прозрачность здесь собрана через color-mix, без отдельного hex на каждую долю. Тот же результат даёт относительный синтаксис, rgb(from var(--main-2) r g b / 50%). Оба способа работают в актуальных Chrome, Safari и Firefox. Зато у полупрозрачного бежевого на тёмно-зелёном фоне появляется имя. В макете это одна из главных ролей цвета, а стиля у неё не было.

Roboto Flex. Одна ось ширины вместо отдельного узкого шрифта
Заголовки набраны вариативным Roboto Flex. Мне был нужен узкий плотный гротеск в верхнем регистре, и вместо отдельного condensed-начертания я взял одну ось. В текстовом стиле Figma сохраняет весь набор осей.
{
"family": "Roboto Flex", "style": "Medium",
"variationSettings": {
"wght": 500, "wdth": 30, "slnt": 0, "GRAD": 0,
"XOPQ": 96, "XTRA": 468, "YOPQ": 79, "YTAS": 750,
"YTDE": -203, "YTFI": 738, "YTLC": 514, "YTUC": 712
}
}Из двенадцати осей я менял две. Ширина wdth стоит на 30 при диапазоне шрифта от 25 до 151, вес wght на 500. Остальные оси, включая параметрические XOPQ, XTRA, YOPQ и прочие с заглавной буквы, остались на значениях по умолчанию. В UI-ките это записано одной строкой «Шрифт для заголовков roboto flex variable weight:500 width:30». Эта часть передачи оказалась самой надёжной, потому что записана числом.
Подключать такой шрифт я бы просил так. Если через Google Fonts, в запросе нужны только оси с диапазонами, остальные и так стоят по умолчанию.
/* пример запроса к Google Fonts */
family=Roboto+Flex:opsz,wdth,wght@8..144,25..151,100..1000Если локальным файлом, в @font-face объявляются диапазоны осей, а сами значения задаются высокоуровневыми свойствами.
/* пример, не код сайта */
@font-face {
font-family: "Roboto Flex";
src: url("/fonts/roboto-flex.woff2") format("woff2");
font-weight: 100 1000;
font-stretch: 25% 151%;
font-display: swap;
}
h1, h2 {
font-family: "Roboto Flex", sans-serif;
font-weight: 500; /* ось wght */
font-stretch: 30%; /* ось wdth, работает, потому что в @font-face объявлен диапазон 25% 151% */
text-transform: uppercase;
}Почему не font-variation-settings. Это свойство задаёт список осей целиком, и каждое следующее объявление, в том числе на дочернем элементе, заменяет весь список, а не дописывает в него. Если задать через него обе оси, а где-то ниже появится font-variation-settings: "wght" 700, ось ширины выпадет из списка и вернётся к значению от font-stretch, по умолчанию 100, и заголовок станет широким. font-weight и font-stretch наследуются и переопределяются независимо. font-variation-settings стоит оставить для осей, у которых нет своего CSS-свойства, вроде XTRA или YTUC. Здесь такие не понадобились.
С оптической осью отдельная история. В текстовом стиле Figma оси opsz в списке нет. В браузере font-optical-sizing: auto, а это значение по умолчанию, подставляет её по кеглю, и на заголовке в 160 px она упирается в максимум шрифта, 144. На 80 px opsz равен 80. В макете значение этой оси никак не зафиксировано, и совпадает ли рисунок букв на 160 px в Figma и в браузере, я тогда не проверял. На крупных заголовках вариативных шрифтов такую сверку стоит делать до вёрстки и записывать её итог в передачу.
И вес файла. Полный вариативный Roboto Flex со всеми осями тяжёлый, а заголовкам нужны ширина 30 и вес 500. Оптический размер в браузере идёт за кеглем, на 160 px это 144, на 80 px это 80, поэтому статических экземпляров два, и вместе они весят в разы меньше полного файла. Вырезаются они инструментом instancer из fontTools. Флаг --static закрепляет все оси, которые не названы в команде, на значениях по умолчанию, без него параметрические оси останутся в файле.
# пример: статические экземпляры под заголовки
fonttools varLib.instancer RobotoFlex.ttf wdth=30 wght=500 opsz=144 --static -o RobotoFlex-160.ttf
fonttools varLib.instancer RobotoFlex.ttf wdth=30 wght=500 opsz=80 --static -o RobotoFlex-80.ttfВыбор вариативного шрифта был моим, значит, и требование к весу должно было стоять в передаче.
Рукописный Sunless Day подключается одним начертанием. Его пара с Roboto Flex держится на интерлиньяже. Стиль «80 Subheader» имеет интерлиньяж 50%, и строка рукописного заходит на гротеск без ручной подгонки.
Стиль Figma | Шрифт | Кегль | Интерлиньяж |
|---|---|---|---|
160 H1 | Roboto Flex, wght 500, wdth 30 | 160 | 80% |
80 H2 | Roboto Flex, wght 500, wdth 30 | 80 | 90% |
160 Subheader | Sunless Day | 160 | 80% |
80 Subheader | Sunless Day | 80 | 50% |
20 | Inter Semi Bold, трекинг −2% | 20 | авто |
В макете между двумя ширинами кегли меняются скачком, 160 на десктопе и 80 на телефоне. Промежуточные ступени я не рисовал. Их стоило записать в токенах, например через clamp(), чтобы разработчику не пришлось их выдумывать.
/* пример: заголовок от 80 px на 390 до 160 px на 1600 */
--text-h1-size: clamp(80px, 54.2px + 6.61vw, 160px);Сетка и низкие экраны
Сетки в макете две, и обе без межколонника.
Сетка | Ширина | Колонки | Межколонник | Поля |
|---|---|---|---|---|
Desktop | 1600 | 4 | 0 | 50 |
Phone | 390 | 2 | 0 | 25 |
Колонки без межколонника задают только края крупных блоков, половину и четверть экрана. Поэтому в токенах сетка сводится к двум числам, полям и максимальной ширине. Точку переключения между полями 25 и 50 в макете я не назначил, а стоило.
Упаковка на первом экране прилипает и едет вместе со страницей пять экранов подряд, и её позиция зависит от высоты окна. В макете было два кадра, 1600×900 и 390×844. Ни один не показывает, что происходит на экране высотой 700 или на телефоне, повёрнутом боком. Если бы я делал страницу заново, к двум ширинам добавил бы хотя бы по одному низкому кадру на каждую, 1440×700 и 844×390.
Прелоадер. Окно в форме яйца
Прелоадер в макете собран двумя кадрами, 1600/01_Loader и 390/01_Loader. Жёлтый фон, в центре силуэт яйца, внутри знак марки. На десктопе яйцо 400×515, на телефоне 340×438. Дальше яйцо раскрывается в первый экран, и это движение показано в 14 кадрах превью и в роликах на мокапах.

Кадры и ролик показывают форму и характер движения, но не дают чисел. Размер яйца в пикселях кадра ничего не говорит о том, каким оно должно быть на окне 1280×720 или 414×896. К роликам нужна была спецификация такого вида.
# пример спецификации прелоадера, как я записал бы её сейчас
preloader:
background: var(--accent-1) # жёлтый, Accent/1
egg:
shape: "вектор Egg 400×515 из кадра 1600/01_Loader, экспорт SVG"
width: { desktop: 25vw, phone: 87vw } # 400 из 1600 и 340 из 390
aspect: 400 / 515
sign: "знак марки по центру яйца, размер в долях ширины яйца"
sequence: # каждому шагу длительность и кривая
- "жёлтый экран с окном-яйцом"
- "знак появляется в яйце"
- "окно раскрывается на весь экран"
- "появляются шапка и первый экран"
scroll_lock: "до конца раскрытия яйца, не дольше"
reduced_motion: "статичный первый экран без заставки"Размер записан в процентах от ширины окна, а не в пикселях кадра. 25% на десктопе и 87% на телефоне это те же 400 из 1600 и 340 из 390, только теперь они пересчитываются под любую ширину окна. На низком окне яйцу нужен ещё предел по высоте. На телефоне боком 87% ширины это 734 px, а высота экрана 390. Окно в яйце технически делается маской по SVG-пути, CSS-маской или вырезом на canvas, этот выбор я оставил бы разработчику и указал только форму.
Длительности шагов в спецификации стоят тоже. В ролике заставка выглядит как спокойная пауза, чтобы рассмотреть знак. В браузере тот же отрезок без реакции на колесо ощущается дольше, поэтому блокировку прокрутки я бы ограничивал концом раскрытия яйца и разрешал прокрутке прервать анимацию. Строка reduced_motion нужна потому, что заставка и прилипающие сцены иначе показываются всем, включая людей, которые отключили анимацию в системе. Статичного варианта первого экрана я не нарисовал, а без него этой строке не на что ссылаться.
Карточка упаковки как запись
Второе требование, новые упаковки без перерисовки, решалось карточкой с одинаковым набором полей. Категория, количество в упаковке, фото, пищевая ценность, срок годности, цвет и вес яйца. Новая упаковка это новая запись, сетка ассортимента от неё не меняется.

Одного поля в макете не хватало. У карточки при наведении проявляется фоновая иконка, и логично красить её в цвет конкретной упаковки. Цветового поля у записи я не завёл, и разных цветов для упаковок в файле нет. Модель карточки с ним выглядела бы так.
// пример модели карточки, написан для статьи
type Pack = {
category: string;
count: number; // яиц в упаковке
photo: string;
nutrition: { kcal: number; protein: number; fat: number; carbs: number };
shelfLifeDays: number;
eggColor: string;
eggWeight: string;
accent: string; // цвет упаковки, красит иконку при наведении
};/* пример: цвет записи приходит на карточку переменной */
.pack { --pack-accent: var(--accent-1); } /* значение по умолчанию */
.pack:hover .pack__icon { color: var(--pack-accent); opacity: 1; }Поле accent у каждой упаковки заполняется из её дизайна, а запасное значение спасает карточку, если цвет забыли указать.

Что было в передаче и чего не хватило
В секции UI Kit макета лежат кнопки и иконки в четырёх состояниях, поля ввода, поведение ссылок («opacity 50% при наведении»), строка про оси Roboto Flex, строка про Inter Semi Bold и ссылка на ролики с анимацией. Рядом 14 превью переходов для десктопа и столько же для телефона.
Надёжнее всего передаётся то, что записано числом. Оси шрифта, поля сетки, интерлиньяжи, цвета стилей. Хуже всего то, что показано только картинкой или видео. Цвета наведения, размер яйца, тайминги, поведение на низком экране. Это разработчику приходится подбирать, и подобранное не обязано совпасть с замыслом.
От чего отказался и что сделал бы иначе
Отказался от переменных Figma в пользу стилей. Для одной страницы стилей хватило, но переменные дали бы режимы и имена, которые разработчик может перенести в CSS один в один. Следующий проект начну с коллекции переменных, даже если страница одна.
Отказался от таблицы таймингов в пользу роликов. Ролик лучше показывает характер движения, но не даёт чисел. Сейчас я бы отдавал оба, ролик для настроения и спецификацию с длительностями и размерами в долях окна, написанную до вёрстки.
Вот что я добавил бы в файл с первого дня.
стили для всех цветов состояний и для полупрозрачного бежевого, с именами под будущие CSS-переменные;
кадры низкого экрана, 1440×700 и телефон в альбомной ориентации;
статичный вариант первого экрана для
prefers-reduced-motion;требование к весу шрифта и просьбу вырезать статический экземпляр вместо полного вариативного файла;
размеры анимированных объектов в процентах от окна, а не только в пикселях кадра;
цветовое поле в модели карточки.
Цифр по продажам у меня нет. Сайт не продаёт напрямую, данные сетей остались у производителя, поэтому через конверсию эту работу не оцениваю.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.