Field
Primitivo de composição de campos de formulário, sem acoplar a nenhuma biblioteca de formulário.
import { Button } from "@blips/ui/components/button";
import { Checkbox } from "@blips/ui/components/checkbox";
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@blips/ui/components/field";
import { Input } from "@blips/ui/components/input";
import { Textarea } from "@blips/ui/components/textarea";
export default function FieldDemo() {
return (
<form className="w-full max-w-md">
<FieldGroup>
<FieldSet>
<FieldLegend>Dados de cobrança</FieldLegend>
<FieldDescription>
Todas as transações são seguras e criptografadas.
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="demo-name">Nome no cartão</FieldLabel>
<Input id="demo-name" placeholder="João da Silva" required />
</Field>
<Field>
<FieldLabel htmlFor="demo-number">Número do cartão</FieldLabel>
<Input
id="demo-number"
placeholder="1234 5678 9012 3456"
required
/>
<FieldDescription>Os 16 dígitos do cartão.</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Endereço</FieldLegend>
<FieldGroup>
<Field orientation="horizontal">
<Checkbox id="demo-same" defaultChecked />
<FieldLabel htmlFor="demo-same" className="font-normal">
Mesmo endereço de entrega
</FieldLabel>
</Field>
<Field>
<FieldLabel htmlFor="demo-notes">Observações</FieldLabel>
<Textarea
id="demo-notes"
placeholder="Alguma observação adicional"
className="resize-none"
/>
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button type="submit">Salvar</Button>
<Button type="button" variant="outline">
Cancelar
</Button>
</Field>
</FieldGroup>
</form>
);
}
Instalação
Uso
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@blips/ui/components/field"<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Nome</FieldLabel>
<Input id="name" placeholder="João da Silva" />
<FieldDescription>Como devemos te chamar.</FieldDescription>
</Field>
</FieldGroup>O Field cuida da associação acessível entre rótulo, controle, descrição e
erro, além do espaçamento e dos layouts (vertical, horizontal e responsivo). Ele
não valida nem controla estado — isso fica com o consumidor (HTML nativo,
TanStack Form, server actions etc.).
Form vs Field
A biblioteca tem dois pontos de entrada para formulários — escolha pelo contexto:
São caminhos independentes — Field não substitui Form nem altera os
formulários existentes baseados em Form. Dentro do react-hook-form você ainda
pode compor Field* via Controller (veja abaixo), usando data-invalid e
FieldError para refletir o fieldState.
Exemplos
Campos básicos
Field empilha rótulo, controle e descrição com espaçamento consistente. A
descrição pode vir antes ou depois do controle.
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
} from "@blips/ui/components/field";
import { Input } from "@blips/ui/components/input";
export default function FieldInput() {
return (
<div className="w-full max-w-md">
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="input-user">Usuário</FieldLabel>
<Input id="input-user" type="text" placeholder="joao.silva" />
<FieldDescription>
Escolha um nome de usuário único para a sua conta.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="input-pass">Senha</FieldLabel>
<Input id="input-pass" type="password" placeholder="••••••••" />
<FieldDescription>
Deve ter pelo menos 8 caracteres.
</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>
</div>
);
}
Estado de erro (sem react-hook-form)
Marque data-invalid no Field e aria-invalid no controle; use FieldError
com errors (array, deduplicado automaticamente) ou children.
"use client";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@blips/ui/components/field";
import { Input } from "@blips/ui/components/input";
import { useState } from "react";
export default function FieldError_() {
const [email, setEmail] = useState("joao");
const invalid = !email.includes("@");
return (
<div className="w-full max-w-md">
<FieldGroup>
<Field data-invalid={invalid}>
<FieldLabel htmlFor="error-email">E-mail</FieldLabel>
<Input
id="error-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={invalid}
placeholder="voce@empresa.com.br"
/>
{invalid ? (
<FieldError errors={[{ message: "Informe um e-mail válido." }]} />
) : (
<FieldDescription>Usaremos para enviar avisos.</FieldDescription>
)}
</Field>
</FieldGroup>
</div>
);
}
Layout horizontal
orientation="horizontal" posiciona o controle ao lado do rótulo. Com
FieldContent, o texto ocupa o espaço e o controle (switch, checkbox) alinha à
direita.
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from "@blips/ui/components/field";
import { Switch } from "@blips/ui/components/switch";
export default function FieldSwitch() {
return (
<div className="w-full max-w-md">
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="field-2fa">
Autenticação em duas etapas
</FieldLabel>
<FieldDescription>
Exige um código adicional ao entrar. Recomendado para contas com
acesso a dados financeiros.
</FieldDescription>
</FieldContent>
<Switch id="field-2fa" />
</Field>
</div>
);
}
Cartões de escolha
Envolva um Field horizontal em FieldLabel dentro de um RadioGroup para
transformar a opção inteira em alvo de clique.
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
FieldTitle,
} from "@blips/ui/components/field";
import { RadioGroup, RadioGroupItem } from "@blips/ui/components/radio-group";
export default function FieldChoiceCard() {
return (
<div className="w-full max-w-md">
<FieldGroup>
<FieldSet>
<FieldLabel htmlFor="plan-group">Plano</FieldLabel>
<FieldDescription>
Escolha o plano que melhor atende à sua operação.
</FieldDescription>
<RadioGroup defaultValue="pro" id="plan-group">
<FieldLabel htmlFor="plan-pro">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
Para times em crescimento, com integrações avançadas.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="pro" id="plan-pro" />
</Field>
</FieldLabel>
<FieldLabel htmlFor="plan-enterprise">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Enterprise</FieldTitle>
<FieldDescription>
Suporte dedicado e SLA para grandes volumes.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="enterprise" id="plan-enterprise" />
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
</FieldGroup>
</div>
);
}
Orientação responsiva
orientation="responsive" é vertical no mobile e horizontal a partir do
breakpoint de contêiner @md do FieldGroup.
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@blips/ui/components/field";
import { Input } from "@blips/ui/components/input";
import { Textarea } from "@blips/ui/components/textarea";
export default function FieldResponsive() {
return (
<form className="w-full max-w-2xl">
<FieldSet>
<FieldLegend>Perfil</FieldLegend>
<FieldDescription>Preencha as informações do perfil.</FieldDescription>
<FieldSeparator />
<FieldGroup>
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="resp-name">Nome</FieldLabel>
<FieldDescription>
Nome completo para identificação.
</FieldDescription>
</FieldContent>
<Input id="resp-name" placeholder="João da Silva" required />
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="resp-message">Mensagem</FieldLabel>
<FieldDescription>
Mantenha curta, de preferência abaixo de 100 caracteres.
</FieldDescription>
</FieldContent>
<Textarea
id="resp-message"
placeholder="Olá, mundo!"
className="min-h-[100px] resize-none sm:min-w-[300px]"
/>
</Field>
</FieldGroup>
</FieldSet>
</form>
);
}
Acessibilidade
- Associe
FieldLabelao controle viahtmlFor/id— clicar no rótulo foca o controle e o leitor de tela anuncia rótulo + descrição. - Para grupos de controles relacionados (checkbox/radio), use
FieldSet+FieldLegendpara expô-los como um único grupo com legenda. - Em estado inválido, marque
aria-invalidno controle e renderizeFieldError(que usarole="alert") para que a mensagem seja anunciada. FieldTitleé um rótulo apenas visual (não é<label>); use-o para títulos de exibição (ex.: cartões de escolha), não para rotular um controle.
Referência da API
Field
Contêiner do campo. Renderiza div[role="group"].
FieldLabel
Rótulo construído sobre Label. Suporta o padrão de cartão de escolha (campo
aninhado).
FieldTitle
Título leve (não é <label>) para cabeçalhos apenas de exibição.
FieldDescription
Texto de apoio do campo. Estiliza links (<a>) automaticamente.
FieldError
Exibe o erro com role="alert".
Com um único erro, renderiza texto simples; com vários, uma lista <ul>.
children tem precedência sobre errors.
FieldGroup
Agrupa campos verticalmente com gap consistente. Habilita container queries
(@container) para a orientação responsiva.
FieldSet / FieldLegend
<fieldset> semântico e sua <legend>. FieldLegend aceita
variant="legend" | "label".
FieldSeparator
Separador horizontal entre grupos, com conteúdo de texto opcional.
FieldContent
Wrapper de conteúdo flexível para layouts horizontais (texto à esquerda, controle à direita).