Blips
Documentação

Field

Primitivo de composição de campos de formulário, sem acoplar a nenhuma biblioteca de formulário.

Carregando…
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

pnpm add @blips/ui

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:

Use Form quando…Use Field quando…
O formulário usa react-hook-form + ZodO formulário não usa react-hook-form
Você quer FormField/Controller e validaçãoÉ config, filtros, editor, protótipo ou HTML nativo
Precisa de aria-invalid/mensagem automáticosVocê controla o estado de erro manualmente (FieldError)

São caminhos independentesField 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.

Carregando…
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.

Carregando…
"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.

Carregando…
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.

Carregando…
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.

Carregando…
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 FieldLabel ao controle via htmlFor/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 + FieldLegend para expô-los como um único grupo com legenda.
  • Em estado inválido, marque aria-invalid no controle e renderize FieldError (que usa role="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"].

PropTipoPadrão
orientation"vertical" | "horizontal" | "responsive""vertical"
data-invalidboolean-
classNamestring-

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".

PropTipoPadrão
errorsArray<{ message?: string } | undefined>-
childrenReact.ReactNode-

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).