Como construir temas dinâmicos no Tailwind CSS v4
- Instalar Tailwind CSS v4 com PostCSS ou o plugin Vite e crie seu arquivo CSS de entrada.
- Cadastre-se tokens de cores semânticas (
primary,surface,accentetc.) usando o@theme inlinediretiva comvar()referências. - Definir valores de cores no modo claro para cada token como propriedades personalizadas CSS em
:root. - Substituir essas propriedades dentro
@media (prefers-color-scheme: dark)para suporte automático ao modo escuro. - Adicionar um
.darkseletor de classe com@custom-variantpara alternância manual do modo escuro via JavaScript. - Criar temas de marca, delimitando o escopo de reatribuições de variáveis em atributos de dados, como
(data-theme="ocean"). - Construir componentes usando apenas utilitários semânticos (
bg-primary,text-on-surface) – nãodark:variantes necessárias. - Trocar temas em tempo de execução, definindo
data-themeno elemento raiz com uma pequena função JavaScript.
Gerenciar temas dinâmicos no Tailwind CSS historicamente significou lutar com marcações repetitivas e detalhadas (por exemplo, bg-blue-600 dark:bg-blue-400 theme-ocean:bg-teal-500 em cada elemento temático). Cada elemento que precisava responder ao modo escuro exigia um dark: prefixo de variante e a introdução de temas de marca em cima disso criaram uma explosão combinatória: um botão que precisa de 3 temas x 2 modos significa 6 variantes de classe por propriedade de cor. Os tokens semânticos Tailwind CSS v4 oferecem uma abordagem fundamentalmente diferente. Ao usar o novo @theme inline diretiva e propriedades personalizadas CSS, os desenvolvedores podem construir um sistema onde uma única classe de utilitário como bg-primary resolve automaticamente para a cor correta em qualquer número de temas e esquemas de cores, sem um único dark: variante na marcação.
Este tutorial percorre a construção desse sistema do zero. No final, os leitores terão uma configuração CSS completa, pronta para copiar e colar, e um componente de cartão de demonstração que se adapta a vários temas de marca e modos claro/escuro.
Índice
Pré-requisitos e configuração do projeto
O que você precisa
Este tutorial requer Tailwind CSS v4instalado por meio do plug-in PostCSS ou da CLI independente. Os leitores devem ter um conhecimento básico das propriedades personalizadas CSS (var() sintaxe) e um ambiente de projeto simples para testar. O scaffold abaixo usa PostCSS, que funciona com projetos HTML simples e a maioria dos bundlers. Para Rapidamente projetos, uso @tailwindcss/vite em vez de @tailwindcss/postcss – instale-o com npm install @tailwindcss/vite e configure-o em vite.config.ts. Para Próximo.jsconfirme se sua versão Next.js oferece suporte à configuração PostCSS antes de continuar.
Você também precisará Node.js 18 ou posterior e um navegador moderno que suporta cores OKLCH (Chrome 111+, Firefox 113+, Safari 15.4+). Se você precisar oferecer suporte a navegadores mais antigos, adicione valores substitutos hexadecimais ou RGB junto com os valores OKLCH mostrados abaixo.
Andaime para um projeto mínimo
Comece com uma instalação limpa para garantir que cada bloco de código subsequente seja colado de forma limpa:
npm install tailwindcss @tailwindcss/postcss @tailwindcss/cli postcss
Verifique a versão instalada:
npx tailwindcss --version
A saída deve mostrar uma versão 4.x, por exemplo tailwindcss v4.3.3 (a versão com a qual testei). Se mostrar 3.x, a instalação não foi direcionada à v4. O @tailwindcss/cli pacote fornece o tailwindcss comando usado para a etapa de construção posteriormente neste artigo.
Crie um postcss.config.mjs na raiz do projeto:
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
Nota: o .mjs extensão requer que seu projeto suporte módulos ES. Se o seu projeto usa CommonJS (não "type": "module" em package.json), renomeie este arquivo para postcss.config.js e substitua export default com module.exports =.
Em seguida, crie o arquivo CSS de entrada (por exemplo, src/app.css) com uma única linha:
@import "tailwindcss";
Esta é a linha de base. Tudo o que segue vai para este arquivo CSS, abaixo da importação.
Compreendendo @theme inline e tokens semânticos no Tailwind v4
O que @theme inline faz
Tailwind v4 @theme directiva substitui a theme seção de tailwind.config.js em CSS. Quando você usa @theme inlineTailwind grava o token var() referência diretamente em cada utilitário gerado em vez de emitir um --color-primary variável de tema e apontando o utilitário para ela. Na saída compilada, bg-surface torna-se background-color: var(--bg-surface, oklch(98% 0.005 260)).
O modelo mental é este: um nome de token registrado como --color-primary dentro @theme inline fica disponível como uma classe utilitária (bg-primary, text-primary, border-primarye assim por diante). O valor atribuído a esse token pode ser ele próprio um var() referência apontando para outra propriedade personalizada CSS definida em outro lugar. Essa indireção é o que torna possível todo o sistema temático. A classe de utilidade permanece fixa; a variável subjacente muda com base no contexto.
Por que os tokens semânticos superam as cores literais
Considere a diferença entre bg-blue-600 dark:bg-blue-400 e simplesmente bg-primary. A primeira abordagem codifica os recursos na marcação. Cada elemento carrega o fardo de saber qual cor deve ser em cada modo. A segunda abordagem codifica a intenção: este elemento deve usar a cor primária, seja lá o que isso signifique no tema e esquema de cores atuais. As decisões de design permanecem totalmente dissociadas da marcação dos componentes.
A documentação oficial do tema Tailwind v4 em https://tailwindcss.com/docs/theme cobre a totalidade @theme API para leitores que desejam explorar além dos tokens coloridos.
Definindo seus tokens de cores semânticas
Escolhendo um vocabulário de token
Um conjunto de tokens bem projetado é pequeno e baseado em funções. O vocabulário a seguir cobre a maioria das necessidades da UI sem se tornar pesado.
primário e no primário formam o par de ação principal: primary é a marca principal ou cor de ação (botões, links, destaques), enquanto on-primary é a cor do texto ou do ícone colocado sobre os planos de fundo primários para garantir o contraste.
Para superfícies de layout, superfície fornece o plano de fundo padrão para cartões, painéis e áreas de página, e na superfície fornece a cor do texto do corpo renderizada nesses planos de fundo.
Os tokens restantes lidam com funções de suporte: sotaque para emblemas, indicadores ou ações secundárias; AVC para cor de borda em divisórias e contornos de entrada; e silenciado para texto suave, como legendas, carimbos de data/hora ou texto auxiliar.
Registrando Tokens com @theme inline
Adicione este bloco à folha de estilo após a importação do Tailwind. Cada var() call inclui um valor substituto para que uma atribuição de variável ausente produza um padrão visível em vez de um elemento transparente:
@theme inline {
--color-primary: var(--bg-primary, oklch(55% 0.25 260));
--color-on-primary: var(--text-on-primary, oklch(100% 0 0));
--color-surface: var(--bg-surface, oklch(98% 0.005 260));
--color-on-surface: var(--text-on-surface, oklch(20% 0.02 260));
--color-accent: var(--bg-accent, oklch(65% 0.2 330));
--color-stroke: var(--border-color, oklch(85% 0.01 260));
--color-muted: var(--text-muted, oklch(55% 0.01 260));
}
Esta é a camada indireta. Tailwind agora gera classes de utilitários (bg-primary, text-on-surface, border-strokeetc.) cujos valores computados apontam para propriedades personalizadas CSS simples, como --bg-primary e --text-on-primary. Essas propriedades ainda não possuem valores de cores concretos. Isso vem a seguir.
Verifique se os nomes dos tokens escolhidos não se sobrepõem ao conjunto de variáveis geradas pelo Tailwind v4 inspecionando a saída compilada. Correr npx tailwindcss -i src/app.css -o out.css e procure por .border-stroke e .text-muted para confirmar que seus utilitários foram gerados. Porque os tokens usam inlineo --color-* os próprios nomes não aparecem na saída.
Criando conjuntos de variáveis claras e escuras
Variáveis de tema padrão (claras)
Defina os valores de cores brutas que os tokens semânticos resolvem por padrão. A sintaxe de cores OKLCH aproveita o tratamento de cores moderno do Tailwind v4 e fornece manipulação de cores perceptualmente uniforme. Não tailwind.config.js obrigatório.
:root {
--bg-primary: oklch(55% 0.25 260);
--text-on-primary: oklch(100% 0 0);
--bg-surface: oklch(98% 0.005 260);
--text-on-surface: oklch(20% 0.02 260);
--bg-accent: oklch(65% 0.2 330);
--border-color: oklch(85% 0.01 260);
--text-muted: oklch(55% 0.01 260);
}
Depois que esse bloco estiver instalado, cada classe de utilidade semântica será resolvida para uma cor concreta de modo de luz.
Substituições do modo escuro com esquema de cores preferenciais
Para oferecer suporte ao modo escuro automático com base na preferência do sistema operacional do usuário, reatribua as mesmas propriedades personalizadas dentro de uma consulta de mídia:
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: oklch(70% 0.2 260);
--text-on-primary: oklch(10% 0.02 260);
--bg-surface: oklch(15% 0.01 260);
--text-on-surface: oklch(90% 0.005 260);
--bg-accent: oklch(75% 0.18 330);
--border-color: oklch(30% 0.02 260);
--text-muted: oklch(60% 0.01 260);
}
}
Isso não requer nenhuma alteração na configuração do Tailwind. Não dark: variante aparece em qualquer lugar do HTML. A consulta de mídia do navegador troca os valores das variáveis e cada utilitário que faz referência a eles é atualizado automaticamente.
Opcional: Adicionando um Dark Toggle Manual com @custom-variant
Para alternar o modo escuro baseado em JavaScript (comum em aplicativos que permitem aos usuários substituir suas preferências de sistema operacional), registre uma variante personalizada e um bloco seletor baseado em classe correspondente. O .dark seletor usa :root.dark com :is() para garantir que tenha maior especificidade do que o @media (prefers-color-scheme: dark) bloco, para que a alternância manual substitua de forma confiável a preferência do sistema operacional, independentemente da ordem de processamento da folha de estilo:
@custom-variant dark (&:where(.dark, .dark *));
:root.dark,
:is(html).dark {
--bg-primary: oklch(70% 0.2 260);
--text-on-primary: oklch(10% 0.02 260);
--bg-surface: oklch(15% 0.01 260);
--text-on-surface: oklch(90% 0.005 260);
--bg-accent: oklch(75% 0.18 330);
--border-color: oklch(30% 0.02 260);
--text-muted: oklch(60% 0.01 260);
}
Isso dá aos leitores ambas as opções. O prefers-color-scheme consulta lida com detecção automática, enquanto o .dark classe suporta alternância explícita. Ambos podem coexistir na mesma folha de estilo. O :is(html).dark seletor tem maior especificidade do que o simples :root dentro da consulta de mídia, garantindo que as alternâncias explícitas sempre vençam.
Adicionando vários temas de marca
Escopo do tema por meio de atributos de dados
O padrão para temas de marca é simples: use um atributo de dados no elemento raiz (por exemplo, (data-theme="ocean")) e reatribuir o mesmo conjunto de propriedades personalizadas dentro desse seletor. Como toda classe de utilitário aponta para os mesmos nomes de variáveis, a marcação nunca muda.
Implementando dois temas adicionais
Os seletores compostos dark+theme abaixo cobrem todos os padrões DOM comuns: quando ambos .dark e data-theme estão no mesmo element, when data-theme é um descendente de .darke quando .dark é um descendente de (data-theme).
(data-theme="ocean") {
--bg-primary: oklch(50% 0.2 230);
--text-on-primary: oklch(100% 0 0);
--bg-surface: oklch(97% 0.01 230);
--text-on-surface: oklch(20% 0.02 230);
--bg-accent: oklch(60% 0.18 180);
--border-color: oklch(82% 0.01 230);
--text-muted: oklch(50% 0.01 230);
}
:is(html).dark(data-theme="ocean"),
:is(html).dark (data-theme="ocean"),
(data-theme="ocean") .dark,
(data-theme="ocean").dark {
--bg-primary: oklch(65% 0.18 230);
--text-on-primary: oklch(10% 0.02 230);
--bg-surface: oklch(12% 0.015 230);
--text-on-surface: oklch(90% 0.005 230);
--bg-accent: oklch(70% 0.15 180);
--border-color: oklch(28% 0.02 230);
--text-muted: oklch(58% 0.01 230);
}
(data-theme="sunset") {
--bg-primary: oklch(60% 0.22 30);
--text-on-primary: oklch(100% 0 0);
--bg-surface: oklch(97% 0.008 30);
--text-on-surface: oklch(22% 0.02 30);
--bg-accent: oklch(70% 0.2 60);
--border-color: oklch(83% 0.01 30);
--text-muted: oklch(52% 0.01 30);
}
:is(html).dark(data-theme="sunset"),
:is(html).dark (data-theme="sunset"),
(data-theme="sunset") .dark,
(data-theme="sunset").dark {
--bg-primary: oklch(68% 0.18 30);
--text-on-primary: oklch(12% 0.02 30);
--bg-surface: oklch(14% 0.012 30);
--text-on-surface: oklch(88% 0.005 30);
--bg-accent: oklch(72% 0.16 60);
--border-color: oklch(30% 0.02 30);
--text-muted: oklch(56% 0.01 30);
}
Adicionar um novo tema de marca é apenas mais um bloco de atribuições variáveis. Os utilitários de marcação permanecem intactos.
Alternando temas em tempo de execução
Um snippet mínimo de JavaScript lida com a alternância de tema e modo escuro. O setTheme A função valida a entrada em uma lista de permissões conhecida para evitar que strings arbitrárias sejam gravadas no atributo DOM. A alternância do modo escuro persiste na preferência do usuário para localStorage e restaura-o no carregamento da página:
const ALLOWED_THEMES = new Set(("default", "ocean", "sunset"));
function setTheme(theme) {
if (!ALLOWED_THEMES.has(theme)) {
console.warn(`setTheme: unknown theme "${theme}" ignored`);
return;
}
document.documentElement.dataset.theme = theme;
}
(function restorePreferences() {
const saved = localStorage.getItem("colorScheme");
if (saved === "dark") document.documentElement.classList.add("dark");
if (saved === "light") document.documentElement.classList.remove("dark");
})();
function toggleDarkMode() {
const isDark = document.documentElement.classList.toggle("dark");
localStorage.setItem("colorScheme", isDark ? "dark" : "light");
}
function clearDarkModePreference() {
localStorage.removeItem("colorScheme");
document.documentElement.classList.remove("dark");
}
Chamando setTheme('ocean') ou toggleDarkMode() é o suficiente para que cada elemento temático da página seja atualizado. O clearDarkModePreference() função remove a preferência armazenada e retorna o controle para o nível do sistema operacional prefers-color-scheme consulta de mídia.
Construindo um componente de cartão adaptativo
Marcação usando apenas utilitários semânticos
Aqui está um componente de cartão com título, corpo de texto e um botão CTA, construído apenas com classes de utilidade semântica. Para usá-lo, coloque essa marcação dentro de um documento HTML vinculado à saída CSS compilada (por exemplo, ):
<div class="bg-surface border border-stroke rounded-2xl p-6 shadow-md max-w-sm">
<h2 class="text-on-surface text-xl font-semibold mb-2">Card Titleh2>
<p class="text-muted text-sm mb-4">
This card adapts to any theme and color scheme automatically, using only semantic utility classes.
p>
<button class="bg-primary text-on-primary px-4 py-2 rounded-lg font-medium hover:opacity-90 transition-opacity">
Action
button>
div>
Não há dark: prefixos. Não há aulas temáticas específicas. A mesma marcação é renderizada corretamente no tema padrão, oceano, pôr do sol e qualquer tema futuro, nos modos claro e escuro.
Resultado visual em todos os temas
Os leitores podem verificar isso colando o CSS e HTML completos em um projeto StackBlitz Vite (com tailwindcss e @tailwindcss/vite instalado). O suporte padrão do CodePen Tailwind tem como alvo v3 e não processará diretivas v4. Alternando data-theme entre ocean e sunset no element and adding or removing the dark classe deve produzir paletas distintamente diferentes a partir de marcações idênticas.
O arquivo CSS do tema completo
A seguir está todo o arquivo CSS montado nas seções anteriores. Ele pode ser incluído em qualquer projeto Tailwind v4 como um sistema de temas único e independente:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme inline {
--color-primary: var(--bg-primary, oklch(55% 0.25 260));
--color-on-primary: var(--text-on-primary, oklch(100% 0 0));
--color-surface: var(--bg-surface, oklch(98% 0.005 260));
--color-on-surface: var(--text-on-surface, oklch(20% 0.02 260));
--color-accent: var(--bg-accent, oklch(65% 0.2 330));
--color-stroke: var(--border-color, oklch(85% 0.01 260));
--color-muted: var(--text-muted, oklch(55% 0.01 260));
}
:root {
--bg-primary: oklch(55% 0.25 260);
--text-on-primary: oklch(100% 0 0);
--bg-surface: oklch(98% 0.005 260);
--text-on-surface: oklch(20% 0.02 260);
--bg-accent: oklch(65% 0.2 330);
--border-color: oklch(85% 0.01 260);
--text-muted: oklch(55% 0.01 260);
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: oklch(70% 0.2 260);
--text-on-primary: oklch(10% 0.02 260);
--bg-surface: oklch(15% 0.01 260);
--text-on-surface: oklch(90% 0.005 260);
--bg-accent: oklch(75% 0.18 330);
--border-color: oklch(30% 0.02 260);
--text-muted: oklch(60% 0.01 260);
}
}
:root.dark,
:is(html).dark {
--bg-primary: oklch(70% 0.2 260);
--text-on-primary: oklch(10% 0.02 260);
--bg-surface: oklch(15% 0.01 260);
--text-on-surface: oklch(90% 0.005 260);
--bg-accent: oklch(75% 0.18 330);
--border-color: oklch(30% 0.02 260);
--text-muted: oklch(60% 0.01 260);
}
(data-theme="ocean") {
--bg-primary: oklch(50% 0.2 230);
--text-on-primary: oklch(100% 0 0);
--bg-surface: oklch(97% 0.01 230);
--text-on-surface: oklch(20% 0.02 230);
--bg-accent: oklch(60% 0.18 180);
--border-color: oklch(82% 0.01 230);
--text-muted: oklch(50% 0.01 230);
}
:is(html).dark(data-theme="ocean"),
:is(html).dark (data-theme="ocean"),
(data-theme="ocean") .dark,
(data-theme="ocean").dark {
--bg-primary: oklch(65% 0.18 230);
--text-on-primary: oklch(10% 0.02 230);
--bg-surface: oklch(12% 0.015 230);
--text-on-surface: oklch(90% 0.005 230);
--bg-accent: oklch(70% 0.15 180);
--border-color: oklch(28% 0.02 230);
--text-muted: oklch(58% 0.01 230);
}
(data-theme="sunset") {
--bg-primary: oklch(60% 0.22 30);
--text-on-primary: oklch(100% 0 0);
--bg-surface: oklch(97% 0.008 30);
--text-on-surface: oklch(22% 0.02 30);
--bg-accent: oklch(70% 0.2 60);
--border-color: oklch(83% 0.01 30);
--text-muted: oklch(52% 0.01 30);
}
:is(html).dark(data-theme="sunset"),
:is(html).dark (data-theme="sunset"),
(data-theme="sunset") .dark,
(data-theme="sunset").dark {
--bg-primary: oklch(68% 0.18 30);
--text-on-primary: oklch(12% 0.02 30);
--bg-surface: oklch(14% 0.012 30);
--text-on-surface: oklch(88% 0.005 30);
--bg-accent: oklch(72% 0.16 60);
--border-color: oklch(30% 0.02 30);
--text-muted: oklch(56% 0.01 30);
}
Coloque esse arquivo em um projeto, aponte a ferramenta de construção para ele e todo o sistema multitemático estará operacional.
Dicas, pegadinhas e práticas recomendadas
Convenções de nomenclatura de token
Mantenha os nomes dos tokens baseados em funções (primary, surface, muted) em vez de baseado na aparência (blue, light-bg). Nomes baseados em aparência anulam o propósito da camada indireta e causarão confusão no momento em que um tema usar um matiz diferente para aquela função.
Modificadores de opacidade
Observação: Modificadores de opacidade como
bg-primary/50trabalhar com esses tokens. Em meu teste com Tailwind v4.3.3, o utilitário compilado paracolor-mix(in oklab, var(--bg-primary, ...) 50%, transparent)com uma planícievar()substituto para navegadores semcolor-mix()apoiar. Versões v4 mais antigas podem se comportar de maneira diferente, portanto verifique sua saída compilada.
Navegadores sem color-mix() o suporte volta para a cor de opacidade total. Se precisar apoiá-los, defina uma variante explícita como um token separado (por exemplo, --color-primary-muted) ou aplique o opacity Propriedade CSS diretamente no elemento.
Considerações de desempenho
O mecanismo de estilo do navegador resolve propriedades personalizadas CSS nativamente, sem execução adicional de JavaScript e sem sobrecarga de renderização mensurável em carregamentos de página típicos. Essa abordagem não requer cálculo de cores em tempo de execução, bibliotecas de recálculo de estilo e provedores de contexto específicos da estrutura. O navegador faz todo o trabalho.
Evitando conflitos com a paleta integrada do Tailwind
Registrando um token chamado --color-red-500 dentro @theme inline entrará em conflito com a escala de cores padrão do Tailwind, produzindo resultados imprevisíveis. Nomes semânticos baseados em funções, como primary e accent reduzem esse risco porque ocupam um namespace diferente da paleta integrada. No entanto, sempre verifique se os nomes escolhidos não colidem inspecionando a saída compilada com npx tailwindcss -i src/app.css -o out.css e procurando seus nomes de token.
Próximas etapas
Tokens semânticos via @theme inline combinado com propriedades personalizadas CSS, suporta temas ilimitados e alterna o modo escuro automaticamente, com marcação livre de classes específicas do tema ou do modo, independentemente de quantos temas existem. A partir daqui, o mesmo padrão se estende naturalmente aos tokens de espaçamento e raio de borda, integra-se bem aos seletores de temas orientados por CMS e é dimensionado para projetar bibliotecas de sistemas. Tente adicionar --spacing-* tokens usando o mesmo @theme inline padrão, então consulte o completo @theme API na documentação do Tailwind v4 para os recursos restantes.
Source link




