Dark mode em skill de design: como escrever as regras de tema sem duplicar tudo

dark mode em skill de design com tokens semânticos de tema claro e escuro
Resposta rápida

Dark mode em skill de design não se resolve listando a paleta duas vezes. A skill descreve o contrato semântico (pares de superfície e foreground, tipo bg-background e text-foreground) e o CSS resolve o valor por tema, com light-dark() mais color-scheme ou com os mesmos tokens redefinidos dentro de .dark. No Tailwind CSS v4 a variante dark usa prefers-color-scheme por padrão, e dá pra trocar por atributo com @custom-variant. A skill ainda guarda as proibições (hex solto, dark: por componente) e a régua de contraste da WCAG: 4.5:1 pra texto normal e 3:1 pra não textual

Fala aí, beleza? Você escreveu uma skill de design bonitinha, listou a paleta inteira, e aí listou de novo a versão escura de cada cor logo abaixo

Aí pediu um card novo e o Claude entregou um componente que fica lindo no claro e simplesmente some no escuro

O problema quase nunca é o modelo, é o formato da regra. Quando a skill descreve COR, ela descreve um valor que só vale num tema. Quando a skill descreve TOKEN semântico, ela descreve um contrato que vale nos dois, e quem resolve o valor final é o CSS

A ideia deste post é essa: um único <code>SKILL.md</code>, um vocabulário de tokens declarado uma vez só, e a troca de tema morando onde ela sempre deveria ter morado 🙂

O que você precisa antes de começar

Nada de PC da Nasa aqui, é bem simples:

  • Claude Code instalado e um projeto onde você já mexe no CSS
  • um projeto com CSS próprio, Tailwind CSS v4 ou shadcn/ui (o passo a passo cobre os três caminhos)
  • decidir onde a skill vai morar: <code>~/.claude/skills/&lt;nome-da-skill&gt;/</code> pra skill pessoal, <code>.claude/skills/&lt;nome-da-skill&gt;/</code> pra skill de projeto
  • saber que a pasta precisa de um <code>SKILL.md</code>, que é o arquivo de entrada obrigatório, com frontmatter YAML no topo

As subpastas <code>scripts/</code>, <code>references/</code> e <code>assets/</code> são opcionais, então não precisa criar nada disso agora

E uma boa notícia pra quem odeia ficar reiniciando terminal: o Claude Code observa os diretórios de skills e reconhece skill nova ou modificada sem reiniciar. Tu salva o arquivo e segue o baile

Se você ainda está em dúvida se essa regra deveria ser uma skill ou não, tem um post aqui do blog sobre onde as regras de design devem morar que resolve essa escolha antes

Formação Claude Code
Formação Recomendada

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 114 aulas
  • 4 projetos
  • 9h 18min

Passo a passo: escrevendo as regras de tema na skill

  1. Crie a pasta e o <code>SKILL.md</code> com frontmatter

O nome da PASTA é o que vira o comando. O campo <code>name</code> do frontmatter é só o rótulo que aparece na listagem, então não adianta caprichar nele achando que muda a invocação

<pre><code class="language-bash">mkdir -p .claude/skills/design-system</code></pre>

E o arquivo:

<pre><code class="language-markdown">— name: Design System description: Define os tokens semânticos de cor e as regras de tema claro e escuro do projeto. Use quando o pedido envolver criar ou alterar componente de UI, cor, borda, fundo ou estado visual. —

# Regras de tema</code></pre>

Repara na <code>description</code>: ela tem capacidade (o que a skill faz) e gatilho (quando usar). Isso não é firula, é o texto que o Claude lê pra decidir se aciona a skill

O erro comum deste passo: escrever <code>description: regras de design</code>. Genérico assim, a skill fica lá parada e o componente sai do jeito que o modelo achou bonito

  1. Declare o vocabulário de tokens uma vez só, em pares

Aqui mora a economia toda. O shadcn/ui organiza cor em pares semânticos: o token base define a superfície, e o token com sufixo <code>-foreground</code> define o texto e os ícones que ficam em cima dela

Token O que ele controla Como aparece na classe
<code>–background</code> a superfície, o fundo <code>bg-background</code>
<code>–foreground</code> texto e ícones sobre a superfície <code>text-foreground</code>
<code>–border</code> a linha de contorno <code>border-border</code>

A regra que você escreve na skill é o par, não a cor:

<pre><code class="language-markdown">## Vocabulário de cor

Toda cor sai de token semântico. Superfície e conteúdo andam em par: se existe --x, existe --x-foreground, e o conteúdo em cima de bg-x usa text-x-foreground

Nunca declare um token só pro tema escuro. O mesmo nome vale nos dois temas</code></pre>

O erro comum deste passo: criar <code>–card-dark</code> ou <code>–texto-escuro</code>. Token com tema no nome é a duplicação voltando pela porta dos fundos

  1. Escreva a troca de tema no CSS, não na skill

A skill diz QUAL token usar. O CSS diz QUANTO vale aquele token em cada tema. São dois trabalhos diferentes e misturar os dois é o que faz sua skill inchar

Caminho 1, com a função light-dark(): ela recebe dois valores de cor e retorna um deles conforme o esquema de cor ativo, sem precisar da media feature <code>prefers-color-scheme</code>

<pre><code class="language-css">:root { color-scheme: light dark;

–background: light-dark(#ffffff, #0b0b0c); –foreground: light-dark(#111111, #f2f2f2); –border: light-dark(#d8d8d8, #2c2c2e); }</code></pre>

Que propriedade é essa <code>color-scheme</code>? Ela é a chave que habilita o <code>light-dark()</code>, e de quebra permite sobrescrever o esquema de cor do usuário em trechos do documento. Sem ela declarada, a função não tem como decidir qual dos dois valores devolver

Sobre suporte: <code>light-dark()</code> está disponível nos três principais motores de navegador e virou Baseline Newly available em 13 de maio de 2024

Caminho 2, o do shadcn/ui: o tema escuro redefine os MESMOS tokens dentro do seletor <code>.dark</code>, em vez de criar tokens novos

<pre><code class="language-css">:root { –background: #ffffff; –foreground: #111111; }

.dark { –background: #0b0b0c; –foreground: #f2f2f2; }</code></pre>

Os dois caminhos entregam a mesma coisa pro componente: <code>bg-background</code> e <code>text-foreground</code> funcionando nos dois temas sem uma linha condicional no JSX

O erro comum deste passo: escolher os dois ao mesmo tempo. Se o valor já é <code>light-dark()</code>, redefinir de novo em <code>.dark</code> só cria duas fontes de verdade brigando

  1. Acerte a estratégia de variante do Tailwind e ligue as variáveis no shadcn/ui

No Tailwind CSS v4, a variante <code>dark</code> usa por padrão a media query <code>prefers-color-scheme</code>. Ou seja: ela segue o sistema operacional do usuário

Se o seu projeto tem um botãozinho de trocar tema, isso não basta, e dá pra trocar a estratégia por atributo com a diretiva <code>@custom-variant</code>:

<pre><code class="language-css">@import "tailwindcss";

@custom-variant dark (&amp;:where([data-theme=dark], [data-theme=dark] *));</code></pre>

O v4 também traz utilitários de <code>color-scheme</code> pra controlar como os elementos renderizam no tema escuro, o que ajuda naqueles componentes nativos que teimam em ignorar seu CSS

Se o projeto é shadcn/ui, o tema por variável CSS depende de <code>tailwind.cssVariables</code> estar como <code>true</code> no <code>components.json</code>:

<pre><code class="language-json">{ "tailwind": { "cssVariables": true } }</code></pre>

O erro comum deste passo: o projeto trocar tema por atributo e a skill não contar isso pro Claude. Ele gera <code>dark:</code> do jeito padrão, você clica no botão e nada acontece

Então escreva a estratégia na skill, em uma linha:

<pre><code class="language-markdown">A troca de tema neste projeto é por atributo (data-theme=dark), configurada via @custom-variant. Não assuma prefers-color-scheme</code></pre>

  1. Escreva as proibições, não só as recomendações

Regra positiva sozinha é fácil de contornar. A parte que segura o componente quebrado é a lista do que NÃO pode:

<pre><code class="language-markdown">## Proibido

  • hex, rgb ou hsl solto dentro de componente: cor só via token
  • token com nome de tema (--algo-dark)
  • dark: por componente quando já existe token semântico pro caso
  • texto sem par de superfície: se você usou bg-x, use text-x-foreground

Antes de entregar o componente

Confira contraste nos dois temas: 4.5:1 para texto normal, 3:1 para texto de grande escala e 3:1 para borda, ícone e indicador de estado</code></pre>

  1. Mande o excedente pra <code>references/</code>

A recomendação oficial de autoria de skills é manter o corpo do <code>SKILL.md</code> abaixo de 500 linhas e dividir o que passar disso em arquivos separados

E tem um critério melhor que "tá grande": quando os contextos são mutuamente exclusivos ou raramente usados juntos, a orientação é manter os caminhos em arquivos distintos, o que reduz uso de tokens

<pre><code>.claude/skills/design-system/ ├── SKILL.md └── references/ ├── tokens.md ├── contraste.md └── tailwind-v4.md</code></pre>

O erro comum deste passo: quebrar em arquivo e esquecer de referenciar ele no corpo. Arquivo que ninguém aponta é arquivo que nunca carrega

Erros comuns: quando o Claude gera componente que só existe em um tema

O componente some no tema escuro

Sintoma: card branco com texto quase branco, ou um bloco que virou um retângulo invisível quando você troca o tema

Causa: cor cravada em hex dentro do componente. Hex é valor fixo, não tem como um valor fixo mudar de tema

Solução: troque por token semântico, <code>bg-background</code> com <code>text-foreground</code>, e deixe o valor no arquivo de tokens

Como prevenir: a proibição do passo 5 escrita na skill. Corrigir na mão depois é enxugar gelo, porque o próximo componente nasce igual

Texto legível no claro e ilegível no escuro

Sintoma: o cinza médio que ficava elegante no fundo branco vira uma mancha no fundo escuro

Causa: o par de tokens foi pensado só pro tema claro. O <code>-foreground</code> do escuro herdou um valor que nunca foi medido contra a nova superfície

Solução: medir contraste nos dois temas. O critério 1.4.3 da WCAG 2.2 exige 4.5:1 para texto e 3:1 para texto de grande escala

Como prevenir: deixa claro na skill que o limiar é limiar mesmo. As razões de contraste da WCAG não devem ser arredondadas pra atingir o critério: 4.499:1 NÃO atende o limite de 4.5:1. Tome cuidado com ferramenta que mostra o valor arredondado pra cima

Borda, ícone e foco invisíveis no escuro

Sintoma: o input existe mas você não vê onde ele começa, e o anel de foco sumiu

Causa: a régua de contraste foi aplicada só em texto. Borda e ícone entraram como "detalhe visual" e ninguém mediu

Solução: o critério 1.4.11 da WCAG exige 3:1 para elementos não textuais, incluindo a informação visual necessária pra identificar componentes de interface e seus estados. Estado de foco entra nessa conta

Como prevenir: ter <code>–border</code> como token de primeira classe, usado via <code>border-border</code>, e não como uma corzinha resolvida no olho dentro de cada componente

A variante <code>dark</code> não dispara

Sintoma: o código tem <code>dark:</code> por todo lado, o botão de trocar tema muda o atributo no HTML, e a tela continua clara

Causa: o projeto troca tema por atributo, mas a variante <code>dark</code> do Tailwind v4 continua no padrão <code>prefers-color-scheme</code>, então ela obedece o sistema operacional e ignora seu botão

Solução: declarar a variante por atributo com <code>@custom-variant</code>, como no passo 4

Como prevenir: escrever a estratégia do projeto dentro da skill. O Claude não adivinha convenção de projeto, ele lê o que você deixou escrito, e quanto mais explícito o contexto que você entrega, melhor sai a resposta, que é a mesma lógica de prompts que aproveitam o Opus

Quando vale separar as regras de tema em arquivos de apoio

Skills seguem divulgação progressiva, em três níveis: o frontmatter fica sempre carregado no contexto, o corpo do <code>SKILL.md</code> só carrega quando o Claude julga a skill relevante, e arquivos referenciados carregam sob demanda

Isso joga a favor do tema. Se você conhece aquele padrão de importar módulo só quando precisa, é bem parecido: o índice fica sempre à mão, o conteúdo pesado só entra quando alguém pede

Casos concretos que valem virar arquivo separado:

  • tabela de tokens longa: design system com muitas superfícies vira uma tabela enorme que só interessa quando o pedido é de cor
  • guia de contraste: os critérios, os limiares e o lembrete de não arredondar cabem melhor em <code>references/contraste.md</code>
  • receitas por framework: CSS puro com <code>light-dark()</code>, Tailwind v4 e shadcn/ui são caminhos mutuamente exclusivos. O projeto usa um. Carregar os três é token jogado fora

Duas notas pra quem vai distribuir isso:

Skill entregue por plugin usa o namespace <code>plugin-name:skill-name</code> e não conflita com skill pessoal ou de projeto. Um <code>my-plugin/skills/deploy/SKILL.md</code> vira <code>/my-plugin:deploy</code>. E o comando <code>claude plugin init</code> cria a pasta em <code>~/.claude/skills/</code> com o manifesto <code>.claude-plugin/plugin.json</code> e um <code>SKILL.md</code> inicial

A outra nota é chata e pega muita gente: fora do Claude Code, a especificação de Agent Skills aceita apenas as propriedades <code>allowed-tools</code>, <code>compatibility</code>, <code>description</code>, <code>license</code>, <code>metadata</code> e <code>name</code>. Seis, e só. Um campo fora da lista faz o empacotamento ou o upload falhar, então se você planeja levar a skill pro claude.ai, não invente campo no frontmatter

Conclusão

O princípio cabe em uma frase: a skill descreve o contrato semântico, o CSS resolve o valor por tema

Quando você separa assim, dark mode em skill de design deixa de ser uma segunda paleta pra manter e vira uma consequência do vocabulário que você já declarou. Um par de tokens, uma regra de troca, zero listagem duplicada

Próximo passo é prático: cria a pasta com o <code>SKILL.md</code>, escreve o vocabulário em pares e as proibições, salva (lembra que ele recarrega sozinho) e pede um componente novo

Aí abre nos dois temas e passa a régua de contraste antes de aceitar o código: 4.5:1 no texto, 3:1 no não textual

Se o componente sobreviver aos dois, sua skill está escrita direito 😀

até o próximo post!

Perguntas frequentes

Posso combinar light-dark() com o seletor .dark do shadcn/ui na mesma variável?

Não é recomendado. Se o valor do token já usa light-dark(), redefinir esse mesmo token dentro de .dark cria duas fontes de verdade brigando pelo mesmo nome. Escolha um caminho só: ou light-dark() no :root, ou a redefinição dentro de .dark, nunca os dois juntos no mesmo token.

Preciso declarar color-scheme mesmo usando só utilitários do Tailwind?

Se você usa a função light-dark() no CSS, sim: color-scheme é a propriedade que habilita essa função e permite sobrescrever o esquema de cor do usuário em trechos do documento. O Tailwind CSS v4 também traz utilitários próprios de color-scheme, que ajudam a controlar como elementos nativos renderizam no tema escuro.

Qual contraste mínimo o dark mode da skill precisa garantir pra passar na WCAG?

Pelo critério 1.4.3, texto normal precisa de contraste mínimo de 4.5:1 e texto de grande escala de 3:1. Pelo critério 1.4.11, elementos não textuais como bordas, ícones e indicadores de estado precisam de pelo menos 3:1. Vale lembrar que esses valores são limiares: 4.499:1 não atende o limite de 4.5:1, então não dá pra arredondar pra cima.

A skill de tema precisa ficar em .claude/skills ou dá pra distribuir como plugin?

Os dois caminhos existem. Skill pessoal fica em ~/.claude/skills/<nome-da-skill>/ e skill de projeto em .claude/skills/<nome-da-skill>/. Se a skill vier de um plugin, ela usa o namespace plugin-name:skill-name e não conflita com as skills pessoais ou de projeto que você já tem.

Como o Claude decide sozinho quando acionar a skill de design system?

Ele lê o campo description do frontmatter, que fica sempre carregado no contexto mesmo antes de a skill ser acionada. Por isso essa description precisa ter a capacidade (o que a skill faz) e o gatilho (quando usar), como no exemplo do passo 1 deste post. Descrição genérica demais faz a skill ficar parada sem ser chamada.

Dá pra criar um token só pro tema escuro, tipo –card-dark?

Não, e esse é o erro comum descrito no passo 2. Nunca se declara um token exclusivo pra um tema: o mesmo nome (–background, –border, e por aí vai) precisa valer nos dois, mudando só o valor resolvido pelo CSS. Token com o tema no nome é a duplicação que este post inteiro tenta evitar voltando pela porta dos fundos.



Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Vibe Coding

Formação Vibe Coding

Do Prompt ao Produto: Crie Software Real com IA

  • 474 aulas
  • 20 projetos
  • 39h 27min

Blog | Mais populares