Fumadocs + Code Hike
Frontend

Tailwind CSS

Framework CSS utility-first para desenvolvimento rápido e customizável

O que é Tailwind CSS?

Tailwind CSS é um framework CSS utility-first que oferece classes utilitárias de baixo nível para construir designs customizados diretamente no HTML. Ao invés de componentes pré-construídos, oferece building blocks para criar interfaces únicas.

Por que utilizamos Tailwind CSS na IngenioLab?

  • Utility-first: Classes pequenas e reutilizáveis
  • Customização total: Controle completo sobre o design
  • Performance: CSS otimizado com purging automático
  • Developer Experience: IntelliSense e hot reload
  • Design System: Consistência através de tokens
  • Responsivo: Mobile-first por padrão

Instalação

# Via NPM
npm install -D tailwindcss postcss autoprefixer
# Inicializar configuração
npx tailwindcss init -p
# Com Vite (automático)
npm create vite@latest meu-projeto -- --template react-ts
npm install -D tailwindcss postcss autoprefixer

Configuração IngenioLab

1. tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
// Cores da IngenioLab
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
900: '#1e3a8a',
},
secondary: {
50: '#f8fafc',
500: '#64748b',
900: '#0f172a',
},
// Cores customizadas
ingeniolab: {
blue: '#007bff',
green: '#28a745',
red: '#dc3545',
}
},
// Fontes
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
// Espaçamentos customizados
spacing: {
'72': '18rem',
'84': '21rem',
'96': '24rem',
},
// Breakpoints customizados
screens: {
'xs': '475px',
'3xl': '1600px',
},
// Animações
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
}
}
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
],
}

2. postcss.config.js:

export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

3. CSS principal:

/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Estilos base customizados */
@layer base {
html {
@apply scroll-smooth;
}
body {
@apply bg-gray-50 text-gray-900;
}
h1, h2, h3, h4, h5, h6 {
@apply font-semibold text-gray-900;
}
}
/* Componentes customizados */
@layer components {
.btn {
@apply px-4 py-2 rounded-lg font-medium transition-colors duration-200;
}
.btn-primary {
@apply bg-primary-500 hover:bg-primary-600 text-white;
}
.btn-secondary {
@apply bg-gray-200 hover:bg-gray-300 text-gray-900;
}
.card {
@apply bg-white rounded-xl shadow-lg border border-gray-200 p-6;
}
.input {
@apply w-full px-3 py-2 border border-gray-300 rounded-lg
focus:ring-2 focus:ring-primary-500 focus:border-primary-500;
}
}
/* Utilitários customizados */
@layer utilities {
.text-balance {
text-wrap: balance;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}

Componentes React com Tailwind

1. Button Component:

// src/components/ui/Button.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/utils/cn'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary-500 text-white hover:bg-primary-600',
destructive: 'bg-red-500 text-white hover:bg-red-600',
outline: 'border border-gray-300 bg-white hover:bg-gray-50',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'hover:bg-gray-100',
link: 'text-primary-500 underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-12 rounded-lg px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
export { Button, buttonVariants }
// Utilitário para classes condicionais
// src/utils/cn.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

2. Card Component:

// src/components/ui/Card.tsx
import { HTMLAttributes, forwardRef } from 'react'
import { cn } from '@/utils/cn'
const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-xl border border-gray-200 bg-white p-6 shadow-lg',
className
)}
{...props}
/>
)
)
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 pb-4', className)}
{...props}
/>
)
)
const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...props}
/>
)
)
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('', className)} {...props} />
)
)
export { Card, CardHeader, CardTitle, CardContent }

3. Layout Components:

// src/components/Layout.tsx
import { ReactNode } from 'react'
interface LayoutProps {
children: ReactNode
}
export const Layout = ({ children }: LayoutProps) => {
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-4">
<h1 className="text-xl font-bold text-gray-900">IngenioLab</h1>
</div>
<nav className="hidden md:flex space-x-8">
<a href="/" className="text-gray-900 hover:text-primary-500 transition-colors">
Home
</a>
<a href="/users" className="text-gray-900 hover:text-primary-500 transition-colors">
Users
</a>
<a href="/dashboard" className="text-gray-900 hover:text-primary-500 transition-colors">
Dashboard
</a>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
{children}
</main>
</div>
)
}
// src/components/Dashboard.tsx
export const Dashboard = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-2xl font-bold text-gray-900">Dashboard</h2>
<div className="mt-4 sm:mt-0">
<button className="btn btn-primary">
New Report
</button>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="card">
<div className="text-sm font-medium text-gray-500">Total Users</div>
<div className="text-3xl font-bold text-gray-900 mt-2">1,234</div>
<div className="text-sm text-green-600 mt-2">+12% from last month</div>
</div>
<div className="card">
<div className="text-sm font-medium text-gray-500">Revenue</div>
<div className="text-3xl font-bold text-gray-900 mt-2">$45,678</div>
<div className="text-sm text-red-600 mt-2">-2% from last month</div>
</div>
<div className="card">
<div className="text-sm font-medium text-gray-500">Orders</div>
<div className="text-3xl font-bold text-gray-900 mt-2">891</div>
<div className="text-sm text-green-600 mt-2">+8% from last month</div>
</div>
<div className="card">
<div className="text-sm font-medium text-gray-500">Conversion Rate</div>
<div className="text-3xl font-bold text-gray-900 mt-2">2.4%</div>
<div className="text-sm text-green-600 mt-2">+0.3% from last month</div>
</div>
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="card">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Revenue Chart</h3>
<div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center">
<span className="text-gray-500">Chart Placeholder</span>
</div>
</div>
<div className="card">
<h3 className="text-lg font-semibold text-gray-900 mb-4">User Growth</h3>
<div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center">
<span className="text-gray-500">Chart Placeholder</span>
</div>
</div>
</div>
</div>
)
}

Formulários com Tailwind

1. Form Components:

// src/components/forms/UserForm.tsx
import { useState } from 'react'
import { Button } from '@/components/ui/Button'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
interface UserFormData {
name: string
email: string
role: string
isActive: boolean
}
export const UserForm = () => {
const [formData, setFormData] = useState<UserFormData>({
name: '',
email: '',
role: 'user',
isActive: true
})
const [errors, setErrors] = useState<Partial<UserFormData>>({})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Validação e envio
console.log('Form data:', formData)
}
return (
<Card className="max-w-2xl mx-auto">
<CardHeader>
<CardTitle>Criar Usuário</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Name Field */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
Nome *
</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
className={`input ${errors.name ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''}`}
placeholder="Nome completo"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Email *
</label>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
className={`input ${errors.email ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''}`}
placeholder="usuario@exemplo.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Role Select */}
<div>
<label htmlFor="role" className="block text-sm font-medium text-gray-700 mb-2">
Role
</label>
<select
id="role"
value={formData.role}
onChange={(e) => setFormData(prev => ({ ...prev, role: e.target.value }))}
className="input"
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="moderator">Moderator</option>
</select>
</div>
{/* Active Checkbox */}
<div className="flex items-center">
<input
id="isActive"
type="checkbox"
checked={formData.isActive}
onChange={(e) => setFormData(prev => ({ ...prev, isActive: e.target.checked }))}
className="h-4 w-4 text-primary-500 focus:ring-primary-500 border-gray-300 rounded"
/>
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
Usuário ativo
</label>
</div>
{/* Actions */}
<div className="flex justify-end space-x-4 pt-4">
<Button variant="secondary" type="button">
Cancelar
</Button>
<Button type="submit">
Criar Usuário
</Button>
</div>
</form>
</CardContent>
</Card>
)
}

Responsividade

1. Grid Responsivo:

// src/components/ResponsiveGrid.tsx
export const ResponsiveGrid = () => {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="card">
<h3 className="text-lg font-semibold mb-2">Card {index + 1}</h3>
<p className="text-gray-600">
Conteúdo do card que se adapta automaticamente ao tamanho da tela.
</p>
</div>
))}
</div>
)
}

2. Navigation Responsiva:

// src/components/ResponsiveNav.tsx
import { useState } from 'react'
export const ResponsiveNav = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false)
return (
<nav className="bg-white shadow-lg">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
{/* Logo */}
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900">IngenioLab</h1>
</div>
{/* Desktop Menu */}
<div className="hidden md:flex items-center space-x-8">
<a href="/" className="text-gray-700 hover:text-primary-500 transition-colors">
Home
</a>
<a href="/users" className="text-gray-700 hover:text-primary-500 transition-colors">
Users
</a>
<a href="/dashboard" className="text-gray-700 hover:text-primary-500 transition-colors">
Dashboard
</a>
<Button size="sm">Login</Button>
</div>
{/* Mobile menu button */}
<div className="md:hidden flex items-center">
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="text-gray-700 hover:text-primary-500 focus:outline-none focus:text-primary-500"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
{/* Mobile Menu */}
{isMenuOpen && (
<div className="md:hidden">
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-gray-50">
<a href="/" className="block px-3 py-2 text-gray-700 hover:text-primary-500">
Home
</a>
<a href="/users" className="block px-3 py-2 text-gray-700 hover:text-primary-500">
Users
</a>
<a href="/dashboard" className="block px-3 py-2 text-gray-700 hover:text-primary-500">
Dashboard
</a>
<div className="px-3 py-2">
<Button size="sm" className="w-full">Login</Button>
</div>
</div>
</div>
)}
</div>
</nav>
)
}

Dark Mode

1. Dark Mode Configuration:

// tailwind.config.js
module.exports = {
darkMode: 'class',
// ... resto da configuração
}

2. Dark Mode Provider:

// src/contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'light' | 'dark'
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window !== 'undefined') {
return (localStorage.getItem('theme') as Theme) || 'light'
}
return 'light'
})
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
root.classList.add(theme)
localStorage.setItem('theme', theme)
}, [theme])
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}

3. Dark Mode Classes:

/* src/index.css */
@layer components {
.card {
@apply bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6;
}
.btn-primary {
@apply bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700 text-white;
}
.input {
@apply w-full px-3 py-2 border border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-700 text-gray-900 dark:text-white
rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500;
}
}

4. Theme Toggle:

// src/components/ThemeToggle.tsx
import { useTheme } from '@/contexts/ThemeContext'
export const ThemeToggle = () => {
const { theme, toggleTheme } = useTheme()
return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
{theme === 'light' ? (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
</svg>
) : (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
)}
</button>
)
}

Plugins Recomendados

1. @tailwindcss/forms:

npm install -D @tailwindcss/forms

2. @tailwindcss/typography:

npm install -D @tailwindcss/typography

3. Uso dos plugins:

// Componente com typography
export const Article = () => {
return (
<article className="prose prose-lg dark:prose-invert max-w-none">
<h1>Título do Artigo</h1>
<p>Conteúdo do artigo com tipografia otimizada...</p>
</article>
)
}

Performance

1. Purging automático:

// tailwind.config.js - já configurado automaticamente
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
"./index.html"
],
// Tailwind remove classes não utilizadas automaticamente
}

2. Class variance authority:

npm install class-variance-authority clsx tailwind-merge