Перейти до вмісту
tatetUI

Токени

Портативний шар кольорів, радіусів і тіней, спільний для 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' відповідно.