Токени
Портативний шар кольорів, радіусів і тіней, спільний для Tailwind v3 і v4.
Уся палітра живе в одному файлі — app/assets/css/tokens.css. Він написаний
чистим CSS без жодного синтаксису Tailwind, тому копіюється у проєкт на v3 і
на v4 без змін.
Чому кожен колір оголошено двічі
--accent: #1d4ed8; --accent-rgb: 29, 78, 216;
Tailwind v4 бере hex і отримує прозорість нативно через color-mix().
Tailwind v3 так не вміє — йому потрібен триплет, який шим withOpacity()
підставляє в rgba(var(--accent-rgb), <alpha>).
Формат саме через кому, а не пробіл: у ваших наявних проєктах
withOpacity() написаний під комовий формат, тож файл падає до них без
правок їхніх конфігів.
Розбіжність між hex і триплетом ловить bun run check:docs.
Іменування
Токени названі за роллю, а не за виглядом. --radius-card, а не
--radius-12. Інакше «зробимо тут трохи кругліше» розповзається по проєкту
без жодного правила, і за пів року в застосунку сім різних радіусів.
Те саме з кольорами: --bg-card замість --white. У темній темі картка не
біла, але лишається карткою.
Пара, яку найчастіше плутають
--accent і --accent-solid — різні токени навмисно:
--accent(#1d4ed8) — для тексту та іконок. На білому дає 6.29:1.--accent-solid(#2563eb) — для заливки кнопки з білим текстом. Дає 4.56:1, тобто рівно AA.
Один відтінок не може одночасно бути читабельним текстом і достатньо контрастною заливкою під білим. Спроба обійтися однією змінною закінчується або блідим текстом, або кнопкою, на якій білий підпис не читається.
Що змінюється в темній темі, а що ні
--accent-solid лишається брендовим і світлішає на hover — на темному
тлі це природніший напрямок реакції, ніж потемніння.
Брендова шкала веде себе неочевидно: 700 лишається темнішим за 600,
бо це hover суцільних кнопок із білим текстом. А 800 і 900 навпаки
світлішають, бо вживаються лише як джерело підкладок із прозорістю
(bg-primary-900/30) — темний відтінок під 30% прозорості над майже чорним
тлом не дав би нічого видимого.
Фокус
--ring і --ring-offset — єдине місце, де задається фокус-кільце. У ваших
чотирьох проєктах воно написане по-різному майже в кожному компоненті
(focus:ring-primary-500/20, outline: 2px solid var(--accent), подекуди
просто focus:outline-none без заміни). Без єдиного токена контракт
доступності неможливо перевірити автоматично.
--ring-offset — це колір під елементом. Він відриває кільце від
заливки, інакше на суцільній кнопці кільце зливається з нею.
Конфіг для Tailwind v3
На v4 реєстрація токенів лежить у theme.css (@theme inline). Для v3
потрібен еквівалент у tailwind.config.js:
const withOpacity = (v) => ({ opacityValue }) =>
opacityValue === undefined ? `rgb(var(${v}))` : `rgba(var(${v}), ${opacityValue})`
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
ink: withOpacity('--ink-rgb'),
muted: withOpacity('--ink-muted-rgb'),
main: withOpacity('--bg-main-rgb'),
card: withOpacity('--bg-card-rgb'),
line: withOpacity('--line-rgb'),
ring: withOpacity('--ring-rgb'),
accent: {
DEFAULT: withOpacity('--accent-rgb'),
solid: withOpacity('--accent-solid-rgb'),
},
},
borderRadius: {
control: 'var(--radius-control)',
card: 'var(--radius-card)',
overlay: 'var(--radius-overlay)',
},
boxShadow: {
card: 'var(--shadow-card)',
raised: 'var(--shadow-raised)',
overlay: 'var(--shadow-overlay)',
},
},
},
}
Назви класів після цього однакові в обох версіях, тому скопійований компонент компілюється без змін.
Дві речі, які на v3 не працюють
@utility scrollbar-thin і @custom-variant dark — синтаксис Tailwind v4.
На v3 перенесіть їх у @layer utilities і darkMode: 'class' відповідно.