← Todos os posts
Tech 8 min de leitura
#shadcn-ui #radix-ui #react #tailwind #frontend

Shadcn UI: componentes que viram código do seu projeto

Como o shadcn/ui se diferencia de bibliotecas como MUI e Chakra, por que ele combina tão bem com Radix UI e como usar em formulários e carrosséis.

20px

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:

AbordagemO componente mora onde?Como customiza?Melhor para
MUI / Chakra / AntDentro de node_modulesTema, props, wrappers e overridesApps que aceitam bem o visual da biblioteca
Headless UI / Radix puroDentro da lib, sem estilo prontoVocê cria toda a camada visualTimes com design system maduro
shadcn/uiDentro do seu projetoEditando o próprio códigoProdutos 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:

  1. Configurar Tailwind e components.json.
  2. Adicionar só os componentes necessários.
  3. Ajustar tokens, radius, fonte, cores e estados.
  4. Criar componentes de domínio por cima: UserInviteDialog, BillingPlanCard, ProjectSwitcher.
  5. Manter components/ui como 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:

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.