Entrada de dados
Controles para coleta de dados do usuário. Todos forward refs para o elemento DOM nativo (compatível com react-hook-form).
O que é esta categoria
Esta página reúne todo o conjunto de controles de formulário do SDK — desde o Input de texto simples até campos especializados como PinInput (OTP), PasswordInput (com medidor de força), RangeSlider (faixa dupla) e Dropzone (arrastar-e-soltar arquivos). Todos compartilham a mesma API de rótulo/erro/tamanho (ver a seção Convenções abaixo) e fazem forward de ref, então plugam direto em react-hook-form sem wrappers extras.
Quando usar: sempre que precisar coletar um valor do usuário. Escolha o controle pelo tipo de dado — texto curto (Input), texto longo (Textarea), uma opção entre poucas (Radio/Select), uma opção entre muitas com busca (Combobox), booleano (Switch/Checkbox), código de verificação (PinInput), número com incremento (StepperInput), etc.
Comece pelas Convenções
Todos os campos aceitam label, helperText, error, required e size da mesma forma. Aprenda essas 5 props uma vez e você sabe usar qualquer campo desta página.
Convenções
label(string ou node) — rótulo acima do campo.helperText— texto auxiliar abaixo; substituído porerrorquando este é setado.error(string) — mensagem de erro; adicionaaria-invalid="true"+ borda vermelha.required— adiciona*no label e propagarequiredno input.size: "sm" | "md" | "lg"— escala de altura/padding/font via tokens density.
Input
Seção form-fields da gallery — rode localmente para interagir.
Texto single-line.
import { Input } from "tempest-react-sdk";
import { Search } from "lucide-react";
<Input label="Email" type="email" placeholder="user@example.com" required />;
<Input label="Buscar" leftIcon={<Search size={16} />} placeholder="palavra-chave" />;
<Input label="Senha" type="password" error="Mínimo 8 caracteres" />;
| Prop | Tipo | Default |
|---|---|---|
label |
string |
— |
helperText |
string |
— |
error |
string |
— |
leftIcon |
ReactNode |
— |
rightIcon |
ReactNode |
— |
size |
"sm" \| "md" \| "lg" |
"md" |
wrapperClassName |
string |
— |
| ... | Todos os atributos de HTMLInputElement exceto size |
— |
Textarea
Multi-linha. Mesma API do Input (sem leftIcon/rightIcon).
import { Textarea } from "tempest-react-sdk";
export function Mensagem() {
return <Textarea label="Mensagem" rows={4} helperText="Máximo 500 caracteres" />;
}
Select
Nativo <select>. Aceita options (lista) ou <option> children. Três variantes: field (default — rótulo, controle, helper e slot de erro), chip (só o controle, para linha de configurações) e bare (o mecanismo sem a vestimenta, para app com identidade própria).
import { Select } from "tempest-react-sdk";
export function Estado() {
return (
<Select
label="UF"
options={[
{ value: "SP", label: "São Paulo" },
{ value: "RJ", label: "Rio de Janeiro" },
]}
/>
);
}
| Prop | Tipo | Default |
|---|---|---|
options |
SelectOption[] |
— |
label |
string |
— |
helperText |
string |
— |
error |
string |
— |
variant |
"field" \| "chip" \| "bare" |
"field" |
caretIcon |
ReactNode |
chevron do SDK |
wrapperClassName |
string |
— |
Variante chip — linha de configurações
Numa linha de configurações — ícone e rótulo à esquerda, controle compacto à direita — o campo completo empilha dois campos rotulados dentro do mesmo item de lista. variant="chip" renderiza só o controle, dimensionado pelo conteúdo:
import { ListTile, Select } from "tempest-react-sdk";
export function LinhaIdioma({ lang, setLang }: { lang: string; setLang: (v: string) => void }) {
return (
<ListTile
leading={<span>🌐</span>}
title="Idioma"
trailing={
<Select
variant="chip"
aria-label="Idioma"
value={lang}
onChange={(e) => setLang(e.target.value)}
options={[
{ value: "pt", label: "Português" },
{ value: "en", label: "English" },
]}
/>
}
/>
);
}
aria-label é obrigatório no chip — e o tipo cobra
A variante remove o único rótulo visível que o componente tinha. O texto da linha (title do ListTile) nomeia a linha, não o <select> — não há relação de acessibilidade entre os dois —, então sem aria-label o leitor de tela anuncia uma combobox sem nome. SelectChipProps declara "aria-label": string como obrigatório, então esquecê-lo é erro de compilação, não achado de auditoria.
É por isso que não vale escrever um <select> na mão
A saída que todo app tomava era um <select> nativo com appearance: none só para manter o formato de chip. Isso duplica o markup deste componente e perde junto o anel de foco por token, o estado desabilitado e o caret — tudo o que já estava resolvido aqui.
A variante field continua idêntica: label, helperText e error só existem nela.
Opções a partir de um mapa — toOptions, withAllOption, withEmptyOption
O rótulo em pt-BR de um enum da API é quase sempre um mapa valor → rótulo. toOptions converte esse mapa na lista que Select, MultiSelect e Combobox pedem, e duas funções põem na frente as entradas que um <select> não tem como representar sozinho:
import { useState } from "react";
import { ALL_OPTION_VALUE, Select, toOptions, withAllOption, withEmptyOption } from "tempest-react-sdk";
const STATUS = { active: "Ativo", paused: "Pausado", archived: "Arquivado" };
const HUMOR = new Map([
[5, "Ótimo"],
[3, "Ok"],
[1, "Ruim"],
]);
export function Filtros() {
const [status, setStatus] = useState(ALL_OPTION_VALUE);
const [humor, setHumor] = useState("");
const filtro = status === ALL_OPTION_VALUE ? {} : { status };
return (
<>
<Select
label="Status"
value={status}
onChange={(e) => setStatus(e.target.value)}
options={withAllOption(toOptions(STATUS))}
/>
<Select
label="Humor"
value={humor}
onChange={(e) => setHumor(e.target.value)}
options={withEmptyOption(toOptions(HUMOR))}
/>
<pre>{JSON.stringify(filtro)}</pre>
</>
);
}
withAllOption(options, label = "Todos")— a entrada"all"("não filtra"). Ela precisa ser uma opção de verdade, não oplaceholder: placeholder lê como "nada escolhido", e ninguém sabe se a coluna está filtrada. Compare comALL_OPTION_VALUE, não com a string.withEmptyOption(options, label = "— Não definido —")— a entrada""de um campo anulável.- Nenhuma das duas muta a lista recebida.
Sem entrada vazia, salvar o formulário grava um valor que ninguém escolheu
Medido no jsdom (que implementa o algoritmo de seleção do HTML): um <select> controlado com value="" e sem opção vazia devolve a primeira opção (select.value === "a"). Abrir um formulário de edição e salvar grava esse valor. O placeholder do Select não resolve: a entrada dele é disabled hidden, então depois de escolher algo o campo nunca mais volta a vazio. Campo anulável usa withEmptyOption.
Objeto reordena chave numérica — use Map para escala
toOptions preserva a ordem de um Map sempre, e a de um objeto exceto nas chaves inteiras, que o JavaScript enumera primeiro e em ordem crescente (OrdinaryOwnPropertyKeys). Medido no Node 24: Object.keys({ 5: "Ótimo", 3: "Ok", 1: "Ruim" }) é ["1", "3", "5"]. Uma escala de humor não é crescente por acaso — passe um Map.
Toda chave vira value string, porque é string o que event.target.value devolve: uma opção com o número 0 volta como "0", e === nunca bateria. Por isso a lista serve em SelectOption[], MultiSelectOption[] e ComboboxOption[].
Variante bare — o mecanismo sem a vestimenta
As partes difíceis de um select customizado são appearance: none e um caret posicionado sobre o controle; a vestimenta (borda, fundo, raio, sombra, altura) é a fácil, e é justamente a que um app com identidade própria não quer. variant="bare" entrega só o mecanismo:
import { Select, toOptions, withAllOption } from "tempest-react-sdk";
const STATUS = { active: "Ativo", paused: "Pausado" };
export function BarraDeFiltros() {
return (
<div style={{ display: "flex", gap: 12 }}>
<Select
variant="bare"
aria-label="Status"
wrapperClassName="filtro-marca"
options={withAllOption(toOptions(STATUS))}
/>
</div>
);
}
.filtro-marca {
flex: 0 0 40%;
height: 44px;
padding: 0 4%;
border-radius: 22px;
background-color: #6d28d9;
color: #fff;
}
- Renderiza um único elemento de casca:
wrapper > select + caret. A casca recebewrapperClassNamee é o item flex — não há outrodiventre ela e a barra, entãoflex: 0 0 40%continua valendo. - Não pinta borda, fundo, raio, sombra nem altura. O
<select>estica na altura da casca. - O
<select>herdabackground-colorecolorda casca. O fundo não é estética: o menu nativo pinta cada<option>a partir do fundo do<select>, então um controle transparente devolveria o popup ao branco do sistema. - O anel de foco (
--tempest-focus-ring-*) é desenhado na casca, por:focus-within, e segue o raio dela. - Como
chip, não há rótulo visível:aria-labelouaria-labelledbyé obrigatório no tipo.
O caret não é capturado pelo CSS do app
Medido no Chrome: uma regra de tela .fieldset svg { width: 18.4px; height: 100% } (especificidade 0-1-1) esticava o caret de 14 para 18,39 px. O slot do caret agora fixa o svg com seletor 0-2-1 e margin: 0, então regra de elemento do app não o alcança.
O caret — caretIcon e os tokens de densidade
O caret é posicionado e dimensionado por dois tokens de densidade, lidos por Select e Combobox:
| Densidade | --tempest-control-caret-offset |
--tempest-control-caret-size |
Altura md |
|---|---|---|---|
compact |
10px | 12px | 34px |
comfortable |
12px | 14px | 40px |
touch |
12px | 16px | 44px |
spacious |
14px | 16px | 44px |
Antes, o caret ficava cravado a 12 px da borda em toda densidade (medido no Chrome, com o controle indo de 34 a 44 px e o raio de 4 a 12 px). A faixa reservada para o caret no padding do controle é calculada dos mesmos tokens, então afastar o caret afasta junto o fim do texto. No comfortable, os pixels são os de antes: 12 px de afastamento e 36 px de faixa no field, 10 px e 30 px no chip.
Afastar o caret da borda é um token, no escopo que você quiser:
.meu-formulario {
--tempest-control-caret-offset: 18px;
}
E trocar o ícone é uma prop — o svg passado é esticado ao --tempest-control-caret-size, então o token, não o width do ícone, decide o tamanho em cada densidade:
import { ChevronsUpDown } from "lucide-react";
import { Select } from "tempest-react-sdk";
export function Ordenacao() {
return (
<Select
label="Ordenar por"
caretIcon={<ChevronsUpDown />}
options={[
{ value: "recent", label: "Mais recentes" },
{ value: "name", label: "Nome" },
]}
/>
);
}
Combobox
Seção inputs-advanced da gallery — rode localmente para interagir.
Quando usar: uma opção entre muitas (dezenas+), onde o usuário precisa digitar para filtrar. Para poucas opções use Select.
Select com busca + filtro. Keyboard nav (↑↓ Enter Esc).
import { useState } from "react";
import { Combobox } from "tempest-react-sdk";
const CIDADES = [
{ value: "sp", label: "São Paulo" },
{ value: "rj", label: "Rio de Janeiro" },
{ value: "bh", label: "Belo Horizonte" },
];
export function Cidade() {
const [city, setCity] = useState("sp");
return (
<Combobox
label="Cidade"
options={CIDADES}
value={city}
onChange={setCity}
filter={(option, query) =>
String(option.label).toLowerCase().includes(query.toLowerCase())
}
/>
);
}
A lista abre em portal
A lista é renderizada em document.body, com a largura do campo, e vira para
cima quando não cabe embaixo. Por isso um Combobox na última linha de um
DataTable — cujo wrapper rola e corta o que passa da borda — mostra a lista
inteira. portal={false} devolve a lista em fluxo.
MultiSelect
Seção inputs-extra da gallery — rode localmente para interagir.
Quando usar: várias opções entre muitas, com busca e chips removíveis. Para uma única opção use Combobox; para poucas opções booleanas use Checkbox.
Multi-select filtrável com chips removíveis. Keyboard nav (↑↓ navega, Enter alterna, Esc fecha, Backspace com query vazia remove o último chip).
import { MultiSelect, type MultiSelectOption } from "tempest-react-sdk";
import { useState } from "react";
function Example() {
const [sel, setSel] = useState<string[]>([]);
const options: MultiSelectOption[] = [
{ value: "sp", label: "São Paulo" },
{ value: "rj", label: "Rio de Janeiro" },
];
return <MultiSelect label="Estados" options={options} value={sel} onChange={setSel} />;
}
| Prop | Tipo | Default |
|---|---|---|
options |
MultiSelectOption[] |
— (obrigatório) |
value |
string[] |
— (obrigatório, controlled) |
onChange |
(value: string[]) => void |
— (obrigatório) |
label |
string |
— |
placeholder |
string |
"Selecione" |
helperText |
string |
— |
error |
string |
— |
disabled |
boolean |
false |
maxItems |
number |
— |
filter |
(option, query) => boolean |
— |
emptyMessage |
string |
"Nenhuma opção encontrada" |
portal |
boolean (lista em document.body) |
true |
className |
string |
— |
MultiSelectOption é { value: string; label: string; disabled?: boolean }.
Checkbox
Seção form-primitives da gallery — rode localmente para interagir.
Single checkbox. Suporta indeterminate.
import { Checkbox } from "tempest-react-sdk";
export function Selecao({
someSelected,
allSelected,
}: {
someSelected: boolean;
allSelected: boolean;
}) {
return (
<>
<Checkbox label="Aceito os termos" />
<Checkbox label="Selecionar todos" indeterminate={someSelected && !allSelected} />
</>
);
}
Radio / RadioGroup
Radio standalone OU agrupado com value único.
import { useState } from "react";
import { Radio, RadioGroup } from "tempest-react-sdk";
export function Plano() {
const [plan, setPlan] = useState("free");
return (
<fieldset>
<legend>Plano</legend>
<RadioGroup value={plan} onChange={setPlan}>
<Radio value="free" label="Grátis" />
<Radio value="pro" label="Pro" />
<Radio value="team" label="Team" />
</RadioGroup>
</fieldset>
);
}
RadioGroup não tem label — o rótulo do grupo é seu
O componente é o container de estado (value, onChange, name) e não
renderiza título nenhum. Um grupo de rádios sem rótulo é anunciado só pelas
opções, e o leitor de tela não diz do que elas são opções. Envolva num
<fieldset> com <legend>, como no exemplo — é o que o HTML já resolve,
e nenhum prop precisa existir para isso.
Switch
Quando usar: ligar/desligar uma preferência com efeito imediato (ex.: notificações). Para opt-in que só vale ao submeter o form (ex.: aceitar termos), prefira Checkbox.
Toggle on/off.
import { useState } from "react";
import { Switch } from "tempest-react-sdk";
export function Emails() {
const [subscribed, setSubscribed] = useState(false);
return (
<Switch
label="Receber emails"
checked={subscribed}
onChange={(event) => setSubscribed(event.target.checked)}
/>
);
}
Switch vs Checkbox — não são intercambiáveis
Switch comunica uma ação que acontece agora; Checkbox comunica um estado que será aplicado depois (no submit). Trocar um pelo outro confunde o usuário sobre quando a mudança tem efeito.
ChipInput
Lista de chips com adição por Enter + dedup automático.
import { useState } from "react";
import { ChipInput } from "tempest-react-sdk";
export function Tags() {
const [tags, setTags] = useState<string[]>([]);
return (
<ChipInput label="Tags" value={tags} onChange={setTags} placeholder="adicione e pressione Enter" />
);
}
SearchBar
Input de busca com clear button + debounce opcional via useDebounce.
import { useState } from "react";
import { SearchBar } from "tempest-react-sdk";
export function Busca() {
const [q, setQ] = useState("");
return <SearchBar value={q} onChange={setQ} placeholder="O que você procura?" />;
}
DatePicker
<input type="date"> (ou time, datetime-local, month) com label/error.
import { useState } from "react";
import { DatePicker } from "tempest-react-sdk";
export function Datas() {
const [date, setDate] = useState("2026-01-15");
const [start, setStart] = useState("2026-01-15T09:00");
return (
<>
<DatePicker
label="Data"
value={date}
onChange={setDate}
mode="date"
min="2025-01-01"
/>
<DatePicker
label="Início"
mode="datetime-local"
value={start}
onChange={setStart}
/>
</>
);
}
DateRangePicker
Quando usar: seleção de um intervalo de datas (início + fim) num calendário. Para uma única data use Calendar.
Calendário de intervalo: o primeiro clique define start, o próximo define end (reordenado automaticamente se for anterior), um terceiro clique recomeça; o dia sob o cursor pré-visualiza o intervalo. Matemática de Date pura, sem dependências.
import { DateRangePicker, type DateRange } from "tempest-react-sdk";
import { useState } from "react";
function Example() {
const [range, setRange] = useState<DateRange>({ start: null, end: null });
return <DateRangePicker value={range} onChange={setRange} numberOfMonths={2} />;
}
| Prop | Tipo | Default |
|---|---|---|
value |
DateRange |
— (obrigatório, controlled) |
onChange |
(range: DateRange) => void |
— (obrigatório) |
numberOfMonths |
number |
2 |
defaultMonth |
Date |
— |
minDate |
Date |
— |
maxDate |
Date |
— |
weekStartsOn |
0 \| 1 |
0 |
className |
string |
— |
DateRange é { start: Date | null; end: Date | null }.
TimePicker
Seção material da gallery — rode localmente para interagir.
Quando usar: escolher um horário em colunas roláveis (estilo "spinner" do Material) — agendamentos, lembretes, janelas de atendimento. Para um campo nativo simples, use o DatePicker com mode="time".
Sempre emite uma string 24h "HH:MM" via onChange, mesmo com use12Hours ligado. O minuteStep controla a granularidade da coluna de minutos.
import { useState } from "react";
import { TimePicker } from "tempest-react-sdk";
function ScheduleField() {
const [t, setT] = useState("09:30");
return (
<TimePicker
label="Horário"
value={t}
onChange={setT}
minuteStep={15}
helperText="Selecione hora e minuto"
/>
);
}
| Prop | Tipo | Default |
|---|---|---|
value |
string (24h "HH:MM") |
— |
onChange |
(value: string) => void |
— |
minuteStep |
number (granularidade) |
5 |
use12Hours |
boolean (colunas 1–12 + AM/PM) |
false |
label |
string |
— |
helperText |
string |
— |
disabled |
boolean |
false |
Sempre 24h na saída
Mesmo com use12Hours (colunas 1–12 + AM/PM), o onChange continua emitindo "HH:MM" em 24h — o display 12h é só visual. Guarde e envie o valor de 24h direto.
FileUpload
Seção advanced da gallery — rode localmente para interagir.
Drag-and-drop + click-to-upload + lista de arquivos.
import { useState } from "react";
import { FileUpload } from "tempest-react-sdk";
export function Anexos() {
const [files, setFiles] = useState<File[]>([]);
return (
<FileUpload
label="Anexar"
accept="image/*"
multiple
value={files}
onChange={setFiles}
maxSize={5 * 1024 * 1024}
/>
);
}
Slider
Quando usar: escolher um único valor numa faixa contínua (volume, brilho, etc.). Para uma faixa de dois valores use RangeSlider.
Slider de thumb único sobre <input type="range"> nativo.
import { Slider } from "tempest-react-sdk";
import { useState } from "react";
function Example() {
const [vol, setVol] = useState(30);
return <Slider value={vol} onChange={setVol} label="Volume" formatValue={(v) => v + "%"} />;
}
| Prop | Tipo | Default |
|---|---|---|
value |
number |
— (obrigatório, controlled) |
onChange |
(value: number) => void |
— (obrigatório) |
min |
number |
0 |
max |
number |
100 |
step |
number |
1 |
label |
string |
— |
helperText |
string |
— |
disabled |
boolean |
false |
formatValue |
(value: number) => string |
— |
aria-label |
string |
— |
className |
string |
— |
Slider compacto: nome acessível sem o label visível
Passar label renderiza um bloco acima da track (rótulo + valor). Onde esse
bloco não cabe — rodapé de uma linha, célula de tabela, toolbar — use
aria-label sozinho:
<Slider value={gain} onChange={setGain} aria-label={`Volume de ${nome}`} />
Sem isso, todo slider da página se anuncia como "Slider" e o leitor de
tela não distingue um do outro. A precedência é
aria-label → label → "Slider", então quem já passava só label não
muda em nada. Envolver o campo num <label> externo não resolve: um
aria-label explícito no input vence na ordem de precedência do nome
acessível.
Dropzone
Quando usar: uma área de arrastar-e-soltar enxuta, quando você só precisa
capturar os arquivos (onDrop) e renderizar a lista/preview por conta própria.
Para um campo pronto com label, lista de arquivos e estilo de formulário, use
FileUpload.
Área drag-and-drop com input de arquivo escondido — clicável e focável por
teclado. Filtra por maxSize antes de chamar onDrop; rejeitados vão em
onReject.
import { useState } from "react";
import { Dropzone } from "tempest-react-sdk";
function Uploader() {
const [files, setFiles] = useState<File[]>([]);
return (
<>
<Dropzone
accept="image/*"
multiple
maxSize={5 * 1024 * 1024}
onDrop={(accepted) => setFiles(accepted)}
onReject={(rejected) => alert(`${rejected.length} arquivo(s) acima de 5 MB`)}
>
Arraste imagens aqui ou clique para selecionar
</Dropzone>
<ul>
{files.map((file) => (
<li key={file.name}>{file.name}</li>
))}
</ul>
</>
);
}
| Prop | Tipo | Default |
|---|---|---|
onDrop |
(files: File[]) => void |
— |
accept |
string |
— |
multiple |
boolean |
true |
maxSize |
number (bytes) |
— |
onReject |
(files: File[]) => void |
— |
disabled |
boolean |
false |
children |
ReactNode |
prompt padrão |
className |
string |
— |
A11y: role="button" + tabIndex (Enter/Espaço abrem o seletor); aria-disabled quando disabled.
RangeSlider
Dual-thumb slider com clamp low ≤ high.
import { useState } from "react";
import { RangeSlider } from "tempest-react-sdk";
export function FaixaDePreco() {
const [range, setRange] = useState<[number, number]>([100, 800]);
return (
<RangeSlider
label="Faixa de preço"
min={0}
max={1000}
step={10}
value={range}
onChange={setRange}
formatValue={([lo, hi]) => `R$ ${lo} – R$ ${hi}`}
/>
);
}
Aceita aria-label pelo mesmo motivo do Slider. Cada thumb continua com nome
próprio — "Faixa de preço (mínimo)" e "Faixa de preço (máximo)" — porque
quem navega de um para o outro precisa saber em qual ponta está.
RatingStars
Radio group de estrelas.
import { useState } from "react";
import { RatingStars } from "tempest-react-sdk";
export function Avaliacao() {
const [rating, setRating] = useState(4);
return (
<>
<RatingStars value={rating} onChange={setRating} max={5} size="md" />
<RatingStars value={4.5} readonly size="lg" />
</>
);
}
PinInput
Quando usar: códigos de verificação curtos (OTP, 2FA, confirmação por SMS/email). Para senhas use PasswordInput.
OTP / one-time-code com N células. Paste, auto-advance, backspace flowback, arrow nav.
Colar o código inteiro funciona
O usuário pode colar 123456 em qualquer célula que o PinInput distribui os dígitos automaticamente — defina type="numeric" para que o teclado mobile abra no modo numérico.
import { PinInput } from "tempest-react-sdk";
export function Codigos({ verify }: { verify: (code: string) => void }) {
return (
<>
<PinInput length={6} type="numeric" onComplete={verify} />
<PinInput length={4} type="alphanumeric" masked autoFocus />
</>
);
}
| Prop | Tipo | Default |
|---|---|---|
length |
number |
6 |
type |
"numeric" \| "alphanumeric" |
"numeric" |
value |
string |
— (controlled) |
defaultValue |
string |
"" |
onChange |
(value: string) => void |
— |
onComplete |
(value: string) => void |
— |
masked |
boolean |
false |
size |
"sm" \| "md" \| "lg" |
"md" |
autoFocus |
boolean |
false |
PasswordInput
Field tipo password com toggle de visibilidade + strength meter opcional (5 níveis).
import { PasswordInput } from "tempest-react-sdk";
export function Senha() {
return <PasswordInput label="Senha" autoComplete="new-password" showStrength />;
}
Helper exposto: estimatePasswordStrength(value) retorna 0-4 (length, case mix, digits, symbols).
Use autoComplete correto
Em telas de cadastro use autoComplete="new-password"; em login use autoComplete="current-password". O valor errado faz o gerenciador de senhas do navegador sugerir/salvar a senha de forma incorreta.
| Prop | Tipo | Default |
|---|---|---|
showStrength |
boolean |
false |
strength |
0 \| 1 \| 2 \| 3 \| 4 (override manual) |
estimatePasswordStrength(value) |
strengthLabels |
[string,string,string,string,string] |
["Muito fraca","Fraca","Razoável","Forte","Excelente"] |
toggleLabels |
{ show, hide } |
{ show: "Mostrar senha", hide: "Esconder senha" } |
StepperInput
+ / − numeric com clamp em min/max.
import { useState } from "react";
import { StepperInput } from "tempest-react-sdk";
export function Quantidades() {
const [qty, setQty] = useState(1);
const [price, setPrice] = useState(50);
return (
<>
<StepperInput value={qty} onChange={setQty} min={1} max={10} />
<StepperInput
value={price}
onChange={setPrice}
step={5}
format={(value) => `R$ ${value}`}
/>
</>
);
}
Form / FormSection / FormRow / FormActions / FormField
Seção forms da gallery — rode localmente para interagir.
Layout wrappers para forms (stack/inline/grid) + integração RHF.
import { Button, Form, FormActions, Input } from "tempest-react-sdk";
export function Cadastro() {
return (
<Form layout="grid" columns={2} gap={4}>
<Input label="Nome" />
<Input label="Email" type="email" />
<FormActions align="end">
<Button type="submit">Salvar</Button>
</FormActions>
</Form>
);
}
Detalhes completos em ../forms.md.
ImageCropper
Seção image-cropper da gallery — rode localmente para interagir.
Quando usar: par natural do
FileUpload— foto de perfil, foto de documento, capa. O app decide a proporção de saída; o usuário só escolhe o que cai dentro dela.
O frame fica parado e a imagem pana e dá zoom atrás dele. É o modelo que um fluxo de avatar quer: por construção não existe recorte fora da proporção.
import { useRef, useState } from "react";
import { Button, FileUpload, ImageCropper, type ImageCropperHandle } from "tempest-react-sdk";
export function AvatarField({ onSave }: { onSave: (blob: Blob) => void }) {
const [files, setFiles] = useState<File[]>([]);
const cropper = useRef<ImageCropperHandle>(null);
return (
<>
<FileUpload value={files} onChange={setFiles} accept="image/*" label="Foto" />
{files[0] && (
<>
<ImageCropper
ref={cropper}
src={files[0]}
aspect={1}
shape="circle"
maxSize={512}
outputType="image/jpeg"
/>
<Button
onClick={async () => {
const blob = await cropper.current?.crop();
if (blob) onSave(blob);
}}
>
Salvar
</Button>
</>
)}
</>
);
}
| Prop | Tipo | Default |
|---|---|---|
src |
File \| Blob \| string |
— |
aspect |
number (largura / altura) |
1 |
maxZoom |
number |
4 |
maxSize |
number (teto da maior aresta exportada, px) |
— |
outputType |
string |
"image/png" |
outputQuality |
number (0–1, tipos com perda) |
0.92 |
shape |
"rect" \| "circle" |
"rect" |
onCropChange |
({ zoom, offset }) => void |
— |
label |
string (nome acessível da área) |
"Área de recorte" |
ref |
Ref<ImageCropperHandle> |
— |
O ref expõe { crop, reset }. O crop() devolve Promise<Blob | null>.
Exporta os pixels originais, não o preview
O recorte é lido do tamanho natural da imagem via canvas. Uma foto de 4000 px recortada num preview de 320 px sai com a resolução do original, não com a do preview — que é o erro mais comum em cropper caseiro.
Use maxSize pra limitar: uma foto de 12 MP recortada pra um avatar de 96 px são
megabytes de desperdício.
Nunca sai borda vazia
A imagem é sempre clampada pra cobrir o frame, em pan e em zoom. É o outro defeito clássico: arrastar ou dar zoom-out até o frame mostrar fundo, e aí a faixa transparente (ou preta) fica assada no arquivo exportado. Aqui é impossível por construção — inclusive ao dar zoom-out, quando um offset que era legal deixa de ser.
Teclado de igual peso
A área de recorte é focável. Setas movem (com Shift movem 4×), +/−
dão zoom, 0 centraliza. Roda do mouse também dá zoom. Um cropper que só
funciona arrastando exclui quem navega por teclado.
crop() devolve null, não lança
Antes da imagem carregar, ou se o navegador recusar o encode, o retorno é null.
Um handler de submit não precisa de try/catch — precisa checar o retorno.
File/Blob viram object URL, e ele é revogado
Trocar de foto ou desmontar o componente revoga a URL anterior. Sem isso, cada re-escolha de foto vazaria a anterior pelo resto da vida do documento.
SignaturePad
Seção capture-media da gallery — rode localmente para interagir.
Quando usar: coletar assinatura de próprio punho — comprovante de entrega, ordem de serviço, termo de aceite. Em campo, no celular, com o dedo.
Canvas com captura por pointer (mouse, dedo e caneta pelo mesmo caminho). Os traços são guardados como listas de pontos e o canvas é redesenhado a partir delas — é isso que torna o undo possível: canvas guarda pixel, não histórico, então remover o último traço significa repintar o resto.
import { Button, SignaturePad, type SignaturePadHandle } from "tempest-react-sdk";
import { useRef, useState } from "react";
export function AssinaturaDaEntrega({ entregaId }: { entregaId: string }) {
const pad = useRef<SignaturePadHandle>(null);
const [vazio, setVazio] = useState(true);
async function enviar() {
const blob = await pad.current?.toBlob("image/png");
if (!blob) return;
const form = new FormData();
form.append("assinatura", blob, `${entregaId}.png`);
await api.post(`/entregas/${entregaId}/assinatura`, form);
}
return (
<>
<SignaturePad
label="Assinatura do cliente"
width={360}
height={180}
onEmptyChange={setVazio}
/>
<Button disabled={vazio} onClick={enviar}>Confirmar entrega</Button>
</>
);
}
| Prop | Tipo | Default | O que faz |
|---|---|---|---|
width |
number |
400 |
Largura da superfície em px CSS. |
height |
number |
160 |
Altura da superfície em px CSS. |
penColor |
string |
cor computada | Cor do traço. O default segue --tempest-text. |
penWidth |
number |
2 |
Espessura do traço. |
disabled |
boolean |
false |
Bloqueia o desenho e esmaece a superfície. |
label |
string |
"Signature" |
Nome acessível do canvas. |
onBegin |
() => void |
— | Chamado no início de cada traço. |
onEnd |
(dataUrl: string) => void |
— | Chamado ao fim de cada traço, com a imagem atual. |
onEmptyChange |
(isEmpty: boolean) => void |
— | Chamado quando a vacuidade muda — ligue no botão de submit. |
showActions |
boolean |
true |
Renderiza os botões Desfazer/Limpar. |
Handle imperativo (ref): clear(), undo(), isEmpty(), toDataURL(type?, quality?), toBlob(type?, quality?).
Suba toBlob(), não toDataURL()
Data URL é base64: ~33% mais bytes, e ainda vira string no meio do seu JSON. toBlob() entrega binário pronto pro FormData.
Nitidez em tela de alta densidade
O buffer do canvas é escalado por devicePixelRatio e o contexto recebe o setTransform correspondente. Sem isso o traço sai borrado no celular — é o defeito clássico de canvas em 1x.
A cor do traço segue o tema
O default lê a cor computada do canvas, que o CSS liga em --tempest-text. Assinatura desenhada no tema escuro sai clara; no claro, escura. Passe penColor só se precisar de tinta fixa (azul de caneta, por exemplo).
A11y
- Sempre use
label— screen readers anunciam o campo. erroradicionaaria-invalid="true"+ descreve viaaria-describedby.requiredpropaga atributorequirednativo + indicador visual*.PinInputcells expõemaria-label="Dígito N"individuais.PasswordInput.toggleusaaria-pressede labelaria-labellocalizada.
Resumo
- Escolha o controle pelo tipo de dado — não force um
Inputonde umSelect,SwitchouPinInputcomunica melhor a intenção. - Todos os campos compartilham
label/helperText/error/required/sizee fazem forward deref→ plugam direto emreact-hook-form. errorsubstituihelperTexte adicionaaria-invalidautomaticamente — não duplique a mensagem.
Páginas relacionadas:
- Validação de formulários —
validateForm,useZodForm, máscaras BR,useViaCEPe o wrapper<FormField>. - Layout —
Form/FormSection/FormRow/FormActionspara estruturar os campos. - Ações —
Buttonpara o submit do formulário. - Status & feedback —
Alert/Toastpara confirmar sucesso ou erro do envio.








