Integrando Tailwind CSS v4 ao PrimeVue com GitHub Copilot: quando usar cada um e como garantir consistência - Parte 2

Integrando Tailwind CSS v4 ao PrimeVue com GitHub Copilot: quando usar cada um e como garantir consistência - Parte 2

30 de junho de 2026

Na primeira parte deste artigo construímos a camada de governança do GitHub Copilot para um projeto Vue 3 com PrimeVue 4: as .instructions.md, o agent especializado e a skill de criação de componentes. Nesta parte, vamos integrar o Tailwind CSS ao projeto e mostrar como os mesmos arquivos de governança que criamos antes são exatamente o que permite executar essa migração de forma segura, padronizada e sem produzir os bugs que normalmente acompanham uma mudança desse tamanho.

→ Sign in

→ Create account

→ Reset password

O problema de migrar um projeto que já tem um sistema de design funcionando para o Tailwind CSS é mais sutil do que parece. Sem um conjunto de regras claras, o desenvolvedor começa a duplicar responsabilidades: cores que antes vinham só de tokens do PrimeVue passam a aparecer também em classes utilitárias do Tailwind, espaçamentos ficam definidos em dois lugares ao mesmo tempo, e componentes diferentes começam a resolver o mesmo problema de formas diferentes. O resultado é um projeto com inconsistência visual crescente, classes mortas acumulando no CSS scoped e bugs de estilo que só aparecem em breakpoints específicos. O GitHub Copilot sem governança amplifica esse problema porque ele vai imitar os padrões inconsistentes que encontrar no código. Com governança, ele faz o oposto: replica o padrão correto em cada novo componente sem precisar ser corrigido.

A primeira coisa que fizemos antes de instalar qualquer pacote foi atualizar o arquivo frontend-standards.instructions.md com uma nova seção chamada Hybrid Governance Rule. Essa seção define, de forma explícita, qual ferramenta é responsável por cada categoria de estilo. Tailwind CSS assume layout estrutural, espaçamentos fluidos, tipografia de conteúdo e alinhamentos responsivos. O CSS scoped com tokens do PrimeVue mantém controle exclusivo sobre glassmorphism, box shadows com color-mix(), backdrop filters e qualquer override interno de componente via :deep(). Hardcode de cor em qualquer forma, seja em classe Tailwind ou em scoped CSS, é estritamente proibido.

Imagem da seção Hybrid Governance Rule no arquivo frontend-standards.instructions.md mostrando a tabela de responsabilidades entre Tailwind CSS e Scoped CSS

Essa regra gravada nas instructions tem um efeito prático imediato. Quando o GitHub Copilot abre um arquivo .vue para editar, ele lê as instructions e sabe que espaçamento de formulário pertence ao template como classe Tailwind, não ao <style scoped>. Sabe que glassmorphism nunca vai para o template. Sabe que cor nunca é hardcoded. Sem esse arquivo, o agente produziria código misturado que o desenvolvedor teria que corrigir manualmente em cada geração. Com ele, a saída já sai no padrão correto.

Em seguida atualizamos o agent front-end-especialize da branch article-v1 em front-end-especialize.agent.md da branch article-v2. O agent ganhou dois novos steps obrigatórios. O Step 2 verifica se o Tailwind CSS está instalado no projeto antes de qualquer ação de migração, e executa o setup completo se não estiver. O Step 3, criado a partir das armadilhas que encontramos durante a migração real, faz uma checagem preventiva em três pontos antes de tocar em qualquer componente: o base.css em busca de font-weight: normal no seletor universal, os <span> dentro de headings que precisam de peso de fonte explícito, e a estrutura do AuthLayout.vue.

Imagem do front-end-especialize.agent.md aberto no editor mostrando os Steps 2 e 3 com as verificações preventivas listadas.

O Step 3 existe porque essas três armadilhas não são óbvias e geram bugs silenciosos que levam tempo para diagnosticar. O font-weight: normal no seletor * do base.css padrão do Vite é o mais traiçoeiro: o Tailwind v4 gera seus utilitários dentro de @layer utilities, e CSS fora de qualquer layer tem prioridade de cascata automaticamente mais alta. O efeito é que font-boldfont-semibold e qualquer outra classe de peso de fonte param de funcionar em todos os elementos do projeto, sem nenhuma mensagem de erro. A correção é remover uma linha, mas sem o Step 3 documentado no agent, o próximo desenvolvedor que criar um novo projeto vai cometer o mesmo erro.

Completando a tríade de governança, criamos o arquivo SKILL.md (create-tailwind-migration). A skill funciona como um manual de tradução: para cada padrão de CSS scoped que existia no projeto, ela define o equivalente na arquitetura híbrida, com blueprints lado a lado mostrando o antes e o depois. Ela também contém o checklist de qualidade que o agent executa ao final de cada refatoração, verificando se nenhum espaçamento ficou em scoped CSS, se as cores semânticas continuam em tokens do PrimeVue e se os overrides :deep() estão intactos.

Segue a skill create-tailwind-migration/SKILL.md mostrando um blueprint lado a lado de migração de campo de formulário e a seção Common Post-Migration Pitfalls:

# MIGRATING VUE + PRIMEVUE TO TAILWIND CSS V4 (HYBRID GOVERNANCE)

This migration skill guides the developer/agent when refactoring layouts and form cards to use Tailwind CSS v4 while maintaining the hybrid visual governance.

---

## Governance Split

1. **Tailwind CSS**: Controls layout (flex, grid), spacing (padding, margin, gap), secondary typography, separators, and inline alignment.
2. **Scoped CSS + Design Tokens**: Controls complex semantic colors (e.g., `var(--p-surface-0)`), Glassmorphism effects (blur, semi-transparent borders), and PrimeVue component deep overrides (`:deep()`).

---

## Side-by-Side Blueprints (Old CSS vs Tailwind Hybrid)

### 1. Auth Layout (Main Centering Shell)

#### Before (Custom CSS)
```vue
<template>
  <div class="auth-layout">
    <slot />
  </div>
</template>

<style scoped>
.auth-layout {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  width: 100%;
  padding: 24px;
  background: linear-gradient(135deg, var(--p-primary-950) 0%, var(--p-surface-950) 100%);
}
</style>
```

#### After (Tailwind Hybrid)
```vue
<template>
  <div class="flex items-center justify-center min-height-screen w-full p-6 auth-bg">
    <slot />
  </div>
</template>

<style scoped>
.auth-bg {
  min-height: 100vh;
  background: linear-gradient(135deg, var(--p-primary-950) 0%, var(--p-surface-950) 100%);
}
</style>
```

---

### 2. Form Fields (Inputs, FloatLabel and Password Wrapper)

Always preserve `position: relative` and width on the PrimeVue input wrapper, while the outer layout uses Tailwind.

#### Before (Custom CSS)
```vue
<template>
  <div class="form-field">
    <FloatLabel>
      <InputText id="email" v-model="email" />
      <label for="email">E-mail</label>
    </FloatLabel>
  </div>
</template>

<style scoped>
.form-field {
  margin-bottom: 20px;
}
.form-field :deep(.p-inputtext) {
  width: 100%;
}
</style>
```

#### After (Tailwind Hybrid)
```vue
<template>
  <div class="mb-5">
    <FloatLabel class="w-full">
      <InputText id="email" v-model="email" class="w-full" />
      <label for="email">E-mail</label>
    </FloatLabel>
  </div>
</template>
```

---

### 3. "OR" Visual Separator

Use Tailwind border and flex utilities instead of manual pseudo-elements in scoped CSS.

#### Before (Custom CSS)
```vue
<template>
  <div class="divider">or</div>
</template>

<style scoped>
.divider {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 20px 0;
  color: var(--p-surface-500);
}
.divider::before, .divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--p-surface-200);
  margin: 0 10px;
}
</style>
```

#### After (Tailwind Hybrid)
```vue
<template>
  <div class="flex items-center my-5 text-sm text-surface-500">
    <div class="flex-1 h-px bg-surface-200" />
    <span class="px-3">or</span>
    <div class="flex-1 h-px bg-surface-200" />
  </div>
</template>
```

---

### 4. Form Card + Scoped Overrides

The card keeps its Glassmorphism class (`glass-card`) via scoped CSS, but all internal spacing, width, and children are controlled by Tailwind utility classes.

#### Before (Custom CSS)
```vue
<template>
  <div class="glass-card">
    <h2 class="title">Sign in</h2>
    <form class="glass-form">
      ...
    </form>
  </div>
</template>

<style scoped>
.glass-card {
  width: 100%;
  max-width: 440px;
  padding: 40px;
  border-radius: 24px;
  background: color-mix(in srgb, var(--p-surface-0) 90%, transparent);
  border: 1px solid color-mix(in srgb, var(--p-surface-0) 30%, transparent);
  backdrop-filter: blur(12px);
}
.title {
  font-size: 24px;
  font-weight: 700;
  text-align: center;
  margin-bottom: 30px;
}
</style>
```

#### After (Tailwind Hybrid)
```vue
<template>
  <div class="w-full max-w-[440px] p-8 glass-card">
    <h2 class="text-2xl font-bold text-center mb-6 text-surface-900">Sign in</h2>
    <form class="flex flex-col gap-4">
      ...
    </form>
  </div>
</template>

<style scoped>
.glass-card {
  border-radius: 24px;
  background: color-mix(in srgb, var(--p-surface-0) 90%, transparent);
  border: 1px solid color-mix(in srgb, var(--p-surface-0) 30%, transparent);
  backdrop-filter: blur(12px);
}
</style>
```

---

## Migration Quality Checklist

- [ ] No padding (`p-`), margin (`m-`), gap (`gap-`), flex, or grid rules remain in `<style scoped>`.
- [ ] Semantic border colors and special backgrounds use scoped CSS with `var(--p-surface-*)` or `color-mix()`.
- [ ] Font sizes, weights, and neutral secondary text colors have moved to Tailwind (e.g., `text-surface-500`, `font-bold`).
- [ ] `:deep(.p-password)` still has `position: relative` and full width in scoped CSS to prevent the eye icon from escaping the input bounds.
- [ ] `npm run type-check` exits 0 with no TypeScript errors.

---

## Common Post-Migration Pitfalls

### Pitfall 1 — `font-weight: normal` on the `*` selector in `base.css`

Vite's default `base.css` includes `font-weight: normal` on the universal selector. Because Tailwind v4 generates utilities inside `@layer utilities`, CSS **outside any layer** has higher cascade priority and silently overrides `font-bold`, `font-semibold`, etc. on **every element**.

**Mandatory action before any migration:** check for and remove that line from `base.css`:

```css
/* ❌ REMOVE */
*, *::before, *::after {
  font-weight: normal;
}
```

### Pitfall 2 — `<span>` elements inside headings need an explicit `font-weight`

`<span>` inherits `font-weight` from its parent, but Tailwind does not automatically inject that weight into children. Whenever an accent `<span>` inside an `<h1 class="font-bold">` should have a different weight, declare it explicitly:

```vue
<!-- Accent span with normal weight inside a bold heading -->
<h1 class="text-5xl font-bold">
  Forgot your<br />
  <span class="font-normal hero-title-accent">password?</span>
</h1>
```

### Pitfall 3 — Layout does not vertically center without a flex wrapper

Applying `min-h-screen` directly to `<main class="grid ...">` **does not center** content in the viewport. Always use the correct structure:

```vue
<!-- ❌ Does NOT vertically center -->
<main class="grid grid-cols-[1.05fr_1fr] min-h-screen ...">
  <slot />
</main>

<!-- ✅ Correctly centered -->
<div class="min-h-screen flex items-center justify-center px-5 py-8 lg:px-6 lg:py-12">
  <main class="grid grid-cols-1 lg:grid-cols-[1.05fr_1fr] gap-10 lg:gap-16 items-center w-full max-w-[480px] lg:max-w-[1140px]">
    <slot />
  </main>
</div>
```

Com a governança atualizada, o setup do Tailwind CSS v4 é direto. Três pacotes formam a stack completa: tailwindcss(node_modules/@tailwindcss/vite/dist/index.d.mts) como motor principal, @tailwindcss/vite como plugin de integração com o Vite que elimina a necessidade de um tailwind.config.js separado, e tailwindcss-primeui como ponte entre os dois sistemas, expondo os tokens do PrimeVue como classes utilitárias do Tailwind.

npm install tailwindcss @tailwindcss/vite tailwindcss-primeui --save-dev

O plugin é registrado em uma linha no vite.config.ts:

As diretivas de importação vão no topo do main.css, antes de qualquer outro arquivo CSS do projeto. A ordem é obrigatória pelo mesmo motivo das armadilhas de cascata que o Step 3 do agent verifica.

Com o setup concluído, o agent executa a migração componente a componente seguindo a skill. O AuthLayout.vue é o exemplo mais claro de eliminação de código redundante. O arquivo tinha um <style scoped> com grid, gaps, paddings e dois blocos @media separados. Tudo isso foi substituído por um template de sete linhas sem nenhuma linha de CSS. A responsividade que antes precisava de dois blocos @media agora está inline com os prefixos do Tailwind, e a centralização vertical que o layout sempre deveria ter teve sua estrutura corrigida com o wrapper flex externo que o Step 3 do agent verifica.

<template>
  <div class="min-h-screen flex items-center justify-center px-5 py-8 lg:px-6 lg:py-12">
    <main class="grid grid-cols-1 lg:grid-cols-[1.05fr_1fr] gap-10 lg:gap-16 items-center w-full max-w-[480px] lg:max-w-[1140px]">
      <slot />
    </main>
  </div>
</template>

Os heroes de cada tela passaram pelo mesmo processo. Mais de 80 linhas de CSS scoped com classes nomeadas para cada detalhe de layout viraram templates onde o Tailwind cuida de tudo que é estrutural. O <style scoped> de cada hero mantém apenas o que é de marca: os tokens de cor via var(--p-primary-*) e var(--p-surface-*), o gradiente do texto de acento e os estados visuais dos ícones. O bg-surface-200 nos separadores e o text-surface-400 nos labels funcionam porque o tailwindcss-primeui expõe esses tokens como classes utilitárias, então a cor continua vindo do sistema de design do PrimeVue sem nenhum hardcode.

<h1 class="text-4xl lg:text-5xl font-bold tracking-tight leading-[1.08] hero-title">
  Sign in to continue<br />
  <span class="font-normal hero-title-accent">your journey.</span>
</h1>

font-normal explícito no <span> é um dos itens que o checklist da skill verifica. Sem ele, o span herda font-bold do pai e o gradiente de acento fica em negrito, quebrando o contraste visual pretendido. Com a skill documentando esse padrão, o agent replica corretamente em cada novo hero que criar.

Os cards de formulário são onde a divisão fica mais clara. O template usa Tailwind para toda a estrutura: flex flex-col gap-7 para espaçar os campos, flex flex-col gap-1.5 para agrupar campo e mensagem de erro, flex items-center gap-3.5 my-1 para o separador OR. O <style scoped> mantém exclusivamente o que não tem equivalente no Tailwind: o glassmorphism com backdrop-filter: blur(18px) e os overrides dos componentes internos do PrimeVue via :deep().

Segue a imagem do LoginCard.vue do style scoped preservando o glassmorphism e os overrides :deep() dos componentes PrimeVue.

O resultado da migração, executada pelo agent com a skill como guia, foi a eliminação de mais de 400 linhas de CSS scoped espalhadas entre seis componentes. Classes duplicadas como .field.field-error.or-separator.or-line.or-label e variações de .header.title.description que apareciam de forma levemente diferente em cada card foram removidas por completo. O espaçamento passou a ser consistente em todos os formulários porque vem de um único sistema de classes, não de valores individuais definidos por cada desenvolvedor em cada componente. Bugs de alinhamento que existiam em breakpoints específicos foram corrigidos como consequência natural da migração, porque as classes responsivas do Tailwind são testadas e documentadas, ao contrário de @media queries escritas manualmente.

Para confirmar que nenhuma das mudanças introduziu conflitos de tipagem, o comando de checagem finaliza o processo:

npm run type-check

Imagem do terminal mostrando a saída do npm run type-check com exit code 0 confirmando que o TypeScript compila sem erros.

O que torna esse resultado reproduzível em outros projetos não é o Tailwind CSS em si, mas os três arquivos de governança que agora carregam o conhecimento acumulado da migração. A Hybrid Governance Rule nas instructions garante que qualquer novo componente gerado pelo Copilot já nasce na arquitetura correta. O Step 3 do agent garante que as armadilhas de cascata e de herança de font-weight são verificadas antes de qualquer refatoração. O checklist da skill garante que nenhum espaçamento ficou perdido no CSS scoped e que os overrides :deep() estão intactos. Toda vez que um desenvolvedor abre o GitHub Copilot para criar uma nova tela ou migrar um componente existente, o agente executa esses passos automaticamente e entrega código que já passou pela mesma verificação que antes dependia de revisão manual.

O design system está governado, o Tailwind integrado e o TypeScript compila sem erros, mas os formulários ainda não falam com ninguém. Na Parte 3 desse artigo, a autenticação real entra em cena.

O código completo deste projeto está disponível no meu repositório no GitHub.

Links e Docs:

Não esqueça de me seguir no LinkedIn para mais conteúdos.

Até a próxima!!!

Confira mais:

Fique por dentro das novidades

Assine nossa newsletter e receba as últimas atualizações e artigos diretamente em seu email.

Assinar gratuitamente