Quando alguém fala “biblioteca de UI em React”, a imagem mental costuma ser sempre a mesma: instalar um pacote, importar Button, Dialog, Select, talvez configurar um tema global, e torcer para o design do produto caber dentro das decisões que a biblioteca já tomou.
O shadcn/ui muda essa relação. Ele não tenta ser uma dependência fechada que manda na sua interface. Ele entrega o código dos componentes para dentro do seu projeto. A partir daí, o componente é seu: você lê, altera, versiona, quebra, melhora e adapta ao design system real da aplicação.
Essa diferença parece pequena na instalação, mas fica enorme depois de três meses de produto.
O que é shadcn/ui
Shadcn UI é uma coleção de componentes bem desenhados, acessíveis e distribuídos por CLI. Você escolhe um componente, roda o comando, e ele copia os arquivos para a sua base de código.
npx shadcn@latest add button dialog form carousel
Depois disso, o botão não vem de node_modules. Ele vive em algo como:
src/components/ui/button.tsx
src/components/ui/dialog.tsx
src/components/ui/carousel.tsx
O resultado é quase o meio-termo ideal entre “não quero desenhar tudo do zero” e “não quero ficar refém de uma biblioteca gigante”.
Diferença para MUI, Chakra e outras libs
MUI, Chakra, Ant Design e Mantine são bibliotecas no sentido clássico: você instala o pacote, importa os componentes e usa a API pública que elas expõem. Isso é ótimo quando você quer velocidade, consistência e um ecossistema pronto.
O custo aparece quando o produto começa a ter opinião visual própria. Um botão precisa de uma animação específica. O Select precisa se comportar um pouco diferente. O design system troca tokens. O modal precisa encaixar em um fluxo estranho. Aí você começa a empilhar overrides, props, wrappers e CSS cada vez mais específico.
Com shadcn/ui, a lógica muda:
| Abordagem | O componente mora onde? | Como customiza? | Melhor para |
|---|---|---|---|
| MUI / Chakra / Ant | Dentro de node_modules | Tema, props, wrappers e overrides | Apps que aceitam bem o visual da biblioteca |
| Headless UI / Radix puro | Dentro da lib, sem estilo pronto | Você cria toda a camada visual | Times com design system maduro |
| shadcn/ui | Dentro do seu projeto | Editando o próprio código | Produtos que querem velocidade sem perder controle |
Na prática, shadcn/ui troca dependência por posse. Você ainda usa pacotes por baixo, mas a camada de UI que o time toca todos os dias fica local.
Onde ele encaixa em projetos reais
Shadcn UI brilha quando o produto precisa de uma interface consistente, mas ainda não tem tempo ou orçamento para construir um design system inteiro do zero.
Eu usaria sem pensar em dashboards internos, SaaS B2B, painéis administrativos, ferramentas de IA, CRMs, backoffices, portais com muitos formulários e qualquer produto React/Next/Astro que precise parecer bem cuidado sem virar um projeto paralelo de design.
O fluxo costuma ser:
- Configurar Tailwind e
components.json. - Adicionar só os componentes necessários.
- Ajustar tokens, radius, fonte, cores e estados.
- Criar componentes de domínio por cima:
UserInviteDialog,BillingPlanCard,ProjectSwitcher. - Manter
components/uicomo base reutilizável, sem regra de negócio.
Essa separação é importante. Button, Input, Dialog e Carousel são infraestrutura visual. “Formulário de cadastro de usuário” já é componente de produto.
E o Radix UI nessa história?
Boa parte dos componentes mais interessantes do shadcn/ui usa Radix UI por baixo. Radix é uma coleção de primitives sem estilo, focada em acessibilidade, teclado, gerenciamento de foco e composição.
Pense em camadas:
Produto
UserRegisterForm, PricingDialog, DashboardSidebar
Componentes locais
Button, Input, Select, Dialog, Popover
shadcn/ui
Código base, padrões de composição, Tailwind, variantes
Radix UI
Comportamento acessível, foco, teclado, ARIA
Radix resolve a parte difícil e invisível: abrir um Dialog com foco correto, navegar em um Select pelo teclado, fechar um Popover na hora certa, expor atributos ARIA adequados. Shadcn UI resolve a parte de ergonomia visual: classes, variantes, composição e aparência inicial.
É por isso que a combinação é tão forte. Você não precisa escolher entre “bonito” e “acessível”. O shadcn/ui pega primitives robustas e entrega uma base estilizada que você consegue modificar.
Exemplo: formulário de cadastro
Imagine uma tela simples de cadastro com nome, email e senha. Em um projeto React, eu normalmente combinaria shadcn/ui com React Hook Form e Zod.
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const cadastroSchema = z.object({
name: z.string().min(2, "Informe seu nome."),
email: z.string().email("Informe um email válido."),
password: z.string().min(8, "Use pelo menos 8 caracteres."),
})
type CadastroData = z.infer<typeof cadastroSchema>
export function CadastroForm() {
const form = useForm<CadastroData>({
resolver: zodResolver(cadastroSchema),
defaultValues: {
name: "",
email: "",
password: "",
},
})
function onSubmit(data: CadastroData) {
console.log("cadastro", data)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input placeholder="Nico Dev" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="nico@email.com" type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Senha</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Criar conta
</Button>
</form>
</Form>
)
}
O ponto principal não é o formulário em si. É que cada parte tem uma responsabilidade clara:
- Zod define a regra.
- React Hook Form gerencia estado e validação.
- shadcn/ui entrega a estrutura visual.
- Os componentes locais continuam editáveis.
Se amanhã o time quiser trocar o visual do erro, adicionar ícone no input ou mudar o espaçamento do formulário inteiro, o código está dentro do projeto. Não existe guerra contra CSS gerado por outra biblioteca.
Exemplo: carrossel rotativo
O carrossel do shadcn/ui é construído sobre Embla Carousel. Isso significa que você ganha swipe, navegação e API de controle sem montar tudo na mão.
Um exemplo comum: um carrossel de benefícios na tela de cadastro.
"use client"
import Autoplay from "embla-carousel-autoplay"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
const slides = [
{
title: "Componentes sob seu controle",
description: "O código fica no repositório e pode seguir o design system do produto.",
},
{
title: "Acessibilidade como base",
description: "Radix cuida de foco, teclado e padrões ARIA em vários componentes.",
},
{
title: "Visual consistente",
description: "Tailwind e variantes mantêm estados e espaçamentos previsíveis.",
},
]
export function BeneficiosCarousel() {
return (
<Carousel
opts={{ loop: true }}
plugins={[
Autoplay({
delay: 4000,
stopOnInteraction: true,
}),
]}
className="w-full"
>
<CarouselContent>
{slides.map((slide) => (
<CarouselItem key={slide.title}>
<div className="rounded-2xl border bg-card p-6">
<h3 className="text-lg font-semibold">{slide.title}</h3>
<p className="mt-2 text-sm text-muted-foreground">
{slide.description}
</p>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Para instalar a base:
npx shadcn@latest add carousel
npm install embla-carousel-autoplay
Cadastro + carrossel na mesma tela
Com os dois blocos, a tela final pode ser simples:
export function CadastroPage() {
return (
<main className="grid min-h-screen gap-8 p-6 lg:grid-cols-[420px_1fr]">
<section className="flex items-center">
<CadastroForm />
</section>
<aside className="hidden items-center lg:flex">
<BeneficiosCarousel />
</aside>
</main>
)
}
Esse exemplo mostra bem a filosofia da ferramenta. Você não instalou um “template de cadastro”. Você montou uma interface com peças pequenas, acessíveis e editáveis.
Quando eu não usaria
Shadcn UI não é resposta universal. Se o time não quer manter código de componentes, uma biblioteca tradicional pode fazer mais sentido. Se o produto precisa de uma implementação visual extremamente específica desde o primeiro dia, talvez seja melhor começar direto por Radix puro, Ariakit ou componentes próprios.
Também vale lembrar: como os arquivos são copiados para o projeto, atualizações não chegam magicamente. Isso é vantagem e responsabilidade ao mesmo tempo. Você não acorda com um breaking change visual vindo de node_modules, mas também precisa acompanhar melhorias manualmente quando quiser.
Resumo prático
Use shadcn/ui quando você quer velocidade sem abrir mão de controle.
Use Radix por baixo quando comportamento acessível importa, especialmente em componentes interativos.
Use uma biblioteca tradicional quando a customização pesada não é prioridade.
E, principalmente, trate os componentes copiados como parte do produto. O maior ganho do shadcn/ui não é ter um botão bonito em cinco minutos. É ter uma base de interface que o time consegue entender, adaptar e evoluir sem pedir permissão para uma dependência.