pular para o conteúdo
cd/
projetos

cd/ui

Componentes React que você instala direto no seu projeto, e dá pra ver quanto cada um pesa.

ver no arver repositório

captura de tela de cd/ui

tl;dr#

O cd/ui junta 32 componentes React e 15 blocos (telas prontas), tudo em Base UI + Tailwind v4, num registry do shadcn. Você instala o código direto no seu projeto e mexe como quiser. O build mede o tamanho de cada componente em gzip; hoje a média fica em uns 695 B.

docs · blocos · performance · código

o problema#

biblioteca de componentes geralmente é uma dependência: você instala, importa e torce pra ela continuar fazendo o que você precisa. quando precisa mudar um detalhe, é briga com a API ou com o CSS.

e quase toda biblioteca diz que é "leve". quase nunca diz quanto.

eu queria o contrário:

a ideia#

três apostas simples:

  1. registry em vez de pacote. o cd/ui não vai pro node_modules. um comando copia o arquivo do componente pro seu projeto, e dali pra frente o código é seu.
  2. medir no build. o tamanho vem do esbuild, não de estimativa, e aparece em cada página das docs.
  3. servidor primeiro. componente sem estado é Server Component e não manda JS. "use client" só entra quando tem estado ou evento.

como fica#

não precisa de shadcn init. você cria um components.json mínimo:

json
{
  "style": "new-york",
  "tailwind": { "css": "src/app/globals.css", "baseColor": "neutral" },
  "aliases": { "components": "@/components", "utils": "@/lib/utils" }
}

e um comando só instala o tema, o utils e todos os componentes:

bash
npx shadcn@latest add https://cd-ui.vercel.app/r/all.json

os blocos têm o all-blocks.json. dá pra instalar um item avulso também, pela URL ou registrando o namespace no components.json:

json
{ "registries": { "@cd": "https://cd-ui.vercel.app/r/{name}.json" } }
bash
npx shadcn@latest add @cd/button @cd/dialog

as dependências (Base UI, e Zod quando precisa) e os outros componentes que ele usa vêm junto. e a home já mostra a ideia do projeto.

home do cd/ui: o prompt mostra a rota atual, Ctrl K abre a busca e a média de 695 B em gzip é medida no build
home do cd/ui: o prompt mostra a rota atual, Ctrl K abre a busca e a média de 695 B em gzip é medida no build

cada componente tem a sua página, com preview, código, API, teclas e notas de acessibilidade. o selo de tamanho e o selo client/server saem da medição do build.

página do Button nas docs: selos com o tamanho em gzip e onde roda, abas de preview e código e o marcador rsc na barra lateral
página do Button nas docs: selos com o tamanho em gzip e onde roda, abas de preview e código e o marcador rsc na barra lateral

além dos componentes, o cd/ui tem 15 blocos: telas inteiras prontas (5 de autenticação, 7 de marketing e 3 de app, tipo login, hero, preços, FAQ e configurações), montadas só com os componentes da biblioteca. um bloco instala com um comando e já leva junto os componentes que usa:

bash
npx shadcn@latest add https://cd-ui.vercel.app/r/login-01.json

a página de cada bloco mostra o preview em desktop, tablet e celular, o código, e abre em tela cheia. as docs e os blocos tão em português e em inglês.

página do bloco login-01: o comando de instalação, as abas de preview e código com os tamanhos desktop, tablet e celular e o link pra abrir em tela cheia
página do bloco login-01: o comando de instalação, as abas de preview e código com os tamanhos desktop, tablet e celular e o link pra abrir em tela cheia

como funciona por trás#

o repositório tem duas fontes de verdade: os arquivos em src/registry/cd/ui/*.tsx (o que é distribuído) e o src/docs/catalog.json (nome, título, categoria e descrição de cada componente). os blocos seguem o mesmo esquema, em src/registry/cd/blocks e src/blocks/catalog.json. o build lê tudo isso e gera o que o site e a CLI precisam.

src/registry/cd/ui/*.tsx ─┬─► build-registry.mjs ─► registry.json ─► shadcn build ─► public/r/*.json
                          │
                          └─► metrics.mjs ─► esbuild + gzip ─► src/docs/metrics.json ─► docs

bun run build roda o registry:build (os dois scripts e o shadcn build) e só depois o next build.

ninguém escreve o registry na mão#

o scripts/build-registry.mjs descobre as dependências lendo os imports do próprio arquivo:

js
function importsOf(file) {
  const src = readFileSync(file, "utf8");
  return [...src.matchAll(/^import\s+(?:type\s+)?[^"']*["']([^"']+)["']/gm)]
    .filter((m) => !/^import\s+type\s/.test(m[0]))
    .map((m) => m[1]);
}

o que não é @/ nem react vira dependência npm. o que é @/registry/cd/ui/outro vira dependência de registry (a URL do outro componente). o item theme copia os tokens do globals.css, os blocos :root e .dark.

no fim: se o componente passa a importar outra coisa, o registry já fica sabendo. não tem como esquecer de atualizar lista nenhuma. com os blocos é igual: os componentes que eles importam viram dependências de registry, então instalar um bloco instala tudo que ele usa.

o que aprendi instalando num projeto de verdade#

quando instalei o cd/ui num projeto de fora, apareceram quatro problemas que o meu próprio site não mostrava:

como o peso é medido#

o scripts/metrics.mjs empacota cada componente com esbuild e mede em gzip:

js
const result = await build({
  entryPoints: [file],
  bundle: true,
  minify: true,
  write: false,
  format: "esm",
  // bibliotecas ficam de fora; o código do cd/ui entra
  external: ["react", "react/*", "react-dom", "@base-ui/react", "@base-ui/react/*", /* … */ "zod", "zod/*"],
});
const code = result.outputFiles[0].contents;
metrics[name] = { gzip: gzipSync(code, { level: 9 }).length, /* … */ };

as bibliotecas ficam de fora de propósito: o Base UI e o Zod já tão no seu projeto ou são compartilhados. o número é o código que entra no seu projeto além delas, que é a parte que o cd/ui controla.

o mesmo script marca cada componente como client ou servidor, só olhando se o arquivo começa com "use client":

js
client: /^["']use client["']/.test(source),

os números do site saem dessa medição. os 32 componentes, do menor pro maior:

componentegziproda onde
skeleton234 Bservidor
kbd282 Bservidor
separator284 Bservidor
card377 Bservidor
avatar383 Bclient
spinner398 Bservidor
textarea451 Bclient
field451 Bclient
badge453 Bservidor
progress465 Bclient
popover478 Bclient
radio-group522 Bclient
input525 Bclient
otp-input539 Bclient
form553 Bclient
auth-shell573 Bservidor
tabs613 Bclient
switch660 Bclient
slider667 Bclient
checkbox688 Bclient
alert776 Bservidor
tooltip794 Bclient
pricing-toggle885 Bclient
dialog898 Bclient
accordion912 Bclient
table930 Bservidor
dropdown-menu1,0 kBclient
logo1,1 kBclient
button1,2 kBclient
select1,2 kBclient
toast1,3 kBclient
password-input1,7 kBclient

média de uns 695 B. 9 dos 32 (alert, auth-shell, badge, card, kbd, separator, skeleton, spinner e table) não mandam JS nenhum.

formulário com Zod sem carregar o Zod inteiro#

o Form importa só o núcleo do Zod, o zod/v4/core:

tsx
import { type $ZodObject, flattenError, type output, safeParse } from "zod/v4/core";

assim ele aceita schema de zod e de zod/mini. você passa o schema, dá name pros Field, e cada campo valida a própria chave quando você sai dele:

tsx
const validateField = (name: string, value: unknown) => {
  const field = schema._zod.def.shape[name];
  if (!field) return null;
  const result = safeParse(field, value);
  return result.success ? null : result.error.issues.map((issue) => issue.message);
};

no envio, roda o schema inteiro. se falhar, os erros vão pro FieldError de cada campo. se passar, o onSubmit recebe os dados já convertidos e com o tipo do schema:

tsx
const schema = z.object({
  email: z.email("e-mail inválido"),
  idade: z.coerce.number().min(18, "precisa ter 18+"),
})

<Form schema={schema} onSubmit={(values) => {
  values.idade // number, não string
}}>
  <Field name="email">…</Field>
  <Field name="idade">…</Field>
</Form>

decisões#

copiar em vez de depender. um componente é um arquivo seu. se o botão precisa de outra variante, você edita o arquivo e pronto. o custo é que atualizar depois fica por sua conta.

Base UI por baixo. foco, teclado e aria vêm do Base UI. o cd/ui coloca o visual e as variantes por cima, e cada página de componente lista as teclas e as notas de acessibilidade.

importar só o que usa. cada componente importa só a parte do Base UI que usa (@base-ui/react/dialog), nunca o pacote inteiro. animação, textarea que cresce e spinner são CSS.

movimento com motivo. o botão afunda 2% em 80 ms quando você clica. no geral é de 80 a 240 ms, curvas fortes, só transform e opacity, e todos respeitam o prefers-reduced-motion. a busca (⌘K) abre e fecha sem animação nenhuma: quem usa atalho abre isso centenas de vezes por dia.

o prompt é a logo. o cabeçalho das docs mostra cd/ui ~/docs/button $ com um cursor piscando: o texto é a logo e ainda diz em que página você tá.

o nome do componente é validado. o nome vira caminho de arquivo e URL, então o build só aceita letra minúscula, número e hífen (/^[a-z][a-z0-9-]*$/).

o que mudou depois#

status e próximos passos#

o cd/ui tá na v0.2, com 32 componentes, 15 blocos, tema claro e escuro e o site de docs no ar, em português e inglês. é open source: licença MIT, guia de contribuição e CI rodando lint, tipos e build em todo pull request. sendo honesto sobre a medição: ela conta o código do cd/ui, não o Base UI nem o Zod que vêm junto.

pra adicionar um componente o caminho é curto: o arquivo em src/registry/cd/ui, a entrada no catálogo, os exemplos e a documentação, e o bun run registry:build gera o resto.