safira-ui
v2.4.0
Published
Biblioteca CSS-first, acessível e progressiva para a web e React.
Maintainers
Readme
Safira UI
Web nativa. Interface clara.
Componentes com identidade visual, HTML semântico e CSS estático. Use em páginas simples ou adicione wrappers React quando eles realmente reduzirem trabalho.
Menos dependência. Mais significado.
A Safira começa onde a web já funciona. O browser cuida da semântica e dos comportamentos nativos; o CSS cria uma identidade coerente; React entra como uma camada opcional de conveniência.
O resultado é uma biblioteca que reduz cerimônia sem esconder a plataforma. Você consegue ler o HTML, personalizar a cascata e entender o que chegará às pessoas e às tecnologias assistivas.
| Princípio | O que significa na prática | | ------------------------ | ---------------------------------------------------------------------------------------- | | CSS-first | O pacote visual é um arquivo CSS estático, sem injeção de estilos ou runtime. | | HTML nativo | Componentes mantêm elementos, semântica e comportamentos oferecidos pelo browser. | | API pequena | O consumidor informa intenção; a biblioteca automatiza IDs, relações e defaults seguros. | | Acessível por padrão | Foco, teclado, contraste, movimento reduzido e cores forçadas fazem parte do núcleo. | | Progressiva | Funciona em HTML puro e oferece wrappers opcionais e tipados para React. |
O diferencial em uma frase
A Safira agiliza a construção da interface sem transformar HTML, CSS e acessibilidade em detalhes internos.
Sua linguagem visual segue um minimalismo técnico: paleta contida, espaço para leitura, tipografia funcional e movimento usado apenas para explicar mudanças. As decisões completas estão em visual-identity.md.
Instalação
Requisitos
- Node.js 20.19 ou superior;
- um projeto web com npm;
- React 18 ou superior apenas se você quiser usar os wrappers React.
React não é obrigatório. O CSS e as classes da Safira podem ser usados em HTML puro, Vite ou qualquer outro bundler que aceite importação de CSS.
1. Adicione o pacote
Na raiz do seu projeto, execute:
npm install safira-ui2. Carregue os estilos
Importe o CSS uma única vez no arquivo de entrada da aplicação, normalmente main.ts, main.tsx, app.ts ou app.tsx:
import "safira-ui/styles.css";Em um projeto Vite com React, o início do src/main.tsx pode ficar assim:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "safira-ui/styles.css";
import { App } from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);Carregue a Safira antes do CSS da aplicação. Assim, classes próprias e utilitários do Tailwind têm prioridade sobre a pré-estilização da biblioteca:
import "safira-ui/styles.css";
import "./app.css";Com Tailwind CSS v4, app.css pode começar normalmente com:
@import "tailwindcss";3. Escolha a forma de uso
Para utilizar apenas o CSS, aplique as classes sf-* no HTML:
<button class="sf-button">Minha primeira ação</button>Para utilizar a integração React, importe os componentes pelo submódulo safira-ui/react:
import { Button } from "safira-ui/react";
export function App() {
return <Button>Minha primeira ação</Button>;
}Se o botão aparecer com o estilo da Safira e apresentar um contorno visível ao receber foco pela tecla Tab, a instalação está funcionando.
Primeiros objetivos para quem está começando
Não é necessário conhecer todos os componentes de uma vez. Esta é a sequência recomendada para aprender a biblioteca construindo uma interface real:
| Objetivo | O que praticar | Resultado esperado |
| ----------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------ |
| 1. Estilizar HTML nativo | Use sf-button, variantes e atributos data-*. | Entender a API CSS sem depender de React. |
| 2. Organizar o layout | Combine Stack, Cluster e Card. | Criar espaçamento consistente sem CSS repetitivo. |
| 3. Montar um formulário | Use Field, labels, descrições e erros. | Construir entradas compreensíveis por teclado e leitor de tela. |
| 4. Comunicar estados | Adicione Toast, Badge, loading e disabled. | Informar sucesso, aviso e erro sem depender apenas de cor. |
| 5. Personalizar a marca | Sobrescreva tokens --sf-*. | Adaptar cores, fontes e raios sem alterar componentes. |
| 6. Validar acessibilidade | Navegue com Tab, aplique zoom e teste tema escuro. | Confirmar que a experiência continua utilizável em diferentes condições. |
Primeiro exercício: uma ação acessível
Comece com um botão nativo e explore somente duas variações:
<div class="sf-cluster">
<button class="sf-button">Salvar alterações</button>
<button class="sf-button" data-variant="secondary">Cancelar</button>
</div>Teste este exercício sem mouse:
- pressione
Tabaté chegar ao primeiro botão; - confirme que o foco está claramente visível;
- pressione
EnterouEspaçopara ativá-lo; - aumente o zoom do navegador e confirme que os controles continuam legíveis.
Segundo exercício: um formulário com React
Depois, deixe a Safira cuidar das relações entre label, ajuda e campo:
import { Button, Field, Stack } from "safira-ui/react";
export function Entrar() {
return (
<form>
<Stack>
<Field
label="E-mail"
type="email"
autoComplete="email"
description="Use o endereço cadastrado na sua conta."
required
/>
<Button type="submit">Entrar</Button>
</Stack>
</form>
);
}Ao concluir esses dois exercícios, você já terá aprendido o fluxo principal: importar o CSS, usar HTML semântico, compor layouts e aproveitar a automação opcional do React.
Comece com HTML
Não é necessário inicializar JavaScript, provider ou sistema de temas:
<button class="sf-button">Continuar</button>
<button class="sf-button" data-variant="secondary">Voltar</button>
<details class="sf-accordion">
<summary class="sf-accordion__summary">Como funciona?</summary>
<div class="sf-accordion__content">O próprio browser controla esta interação.</div>
</details>Para páginas sem bundler, use o arquivo compilado dist/safira.css distribuído pelo pacote.
Use a mesma base no React
Os wrappers React não usam CSS-in-JS. Eles renderizam HTML nativo com as mesmas classes públicas:
import "safira-ui/styles.css";
import { Button, Field, Stack } from "safira-ui/react";
export function Cadastro() {
return (
<form>
<Stack>
<Field label="E-mail" type="email" description="Usaremos apenas para contato." required />
<Button type="submit">Criar conta</Button>
</Stack>
</form>
);
}Neste exemplo, Field cria e conecta automaticamente o id, o label, a descrição e um eventual erro por meio de aria-describedby.
Componentes
| Componente | Elemento ou recurso nativo | Responsabilidade |
| ------------- | -------------------------- | ------------------------------------------------------------ |
| Button | <button> | Variantes, tamanhos e carregamento acessível. |
| Field | <label> + <input> | Nome acessível, ajuda, erro e validação. |
| Toast | <div> | Notificação visual, anúncio acessível e fechamento opcional. |
| ToastRegion | <div> | Posicionamento e empilhamento visual de notificações. |
| Badge | <span> | Estado textual compacto. |
| Card | <article> | Agrupamento semântico de conteúdo. |
| Stack | <div> | Composição vertical com escala de espaçamento. |
| Cluster | <div> | Composição horizontal responsiva. |
| Grid | <div> | Grade fixa ou fluida com espaçamento por tokens. |
| Image | <img> | Imagem responsiva com fontes de alta resolução. |
| Accordion | <details> + <summary> | Disclosure sem JavaScript da biblioteca. |
| CodeBlock | <pre> + <code> | Exemplos HTML/TS com realce, cópia e expansão acessível. |
| Modal | <dialog> | Diálogo modal, foco nativo e fechamento acessível. |
| Popover | Popover API | Abertura, fechamento e light dismiss declarativos. |
| SkipLink | <a> | Salto direto para o conteúdo principal. |
Cada componente visual também pode ser usado diretamente por sua classe sf-*.
Toast visual
Toast cuida da apresentação e da semântica de uma notificação, sem impor provider, fila global ou temporizador. O consumidor mantém o controle sobre quando a mensagem aparece e desaparece:
import { useState } from "react";
import { Toast, ToastRegion } from "safira-ui/react";
export function Notifications() {
const [visible, setVisible] = useState(true);
return visible ? (
<ToastRegion position="top-end">
<Toast title="Perfil salvo" tone="success" onDismiss={() => setVisible(false)}>
Suas alterações já estão disponíveis.
</Toast>
</ToastRegion>
) : null;
}Quando onDismiss é informado, o toast exibe uma barra regressiva e desaparece após 5 segundos. Altere esse tempo com duration={8000} ou use duration={0} para desativar o fechamento automático. O progresso e o temporizador pausam enquanto o usuário mantém o ponteiro ou o foco dentro da notificação.
Os tons info, success e warning usam role="status"; danger usa role="alert". Use live={false} quando o conteúdo já tiver sido anunciado por outro meio. Em HTML puro, combine sf-toast-region, sf-toast, data-position e data-tone; JavaScript só é necessário para ações como fechar ou remover automaticamente.
Accordion com uma única tag
O consumidor utiliza somente Accordion; a Safira cria internamente os elementos nativos <details> e <summary>. className e styles são objetos que personalizam cada parte do componente:
const [open, setOpen] = useState(false);
<Accordion
summary="Como funciona?"
open={open}
disabled={false}
animationDuration={300}
className={{
container: "minha-borda",
button: "meu-resumo",
chevron: "meu-chevron",
content: "meu-conteudo",
}}
styles={{
container: { borderRadius: "1rem" },
button: { fontWeight: 800 },
chevron: { color: "rebeccapurple" },
content: { padding: "1rem" },
}}
onChange={(event) => setOpen(event.open)}
onOpen={(event) => console.log("Abriu", event)}
onClose={(event) => console.log("Fechou", event)}
>
Conteúdo do accordion
</Accordion>;Quando open é informado, o componente é controlado e espera que onChange atualize o estado. Sem open, <details> controla a abertura nativamente. animationDuration é medida em milissegundos e aceita qualquer número igual ou maior que zero.
Blocos de código reutilizáveis
CodeBlock apresenta exemplos em HTML, TypeScript e terminal com realce de sintaxe, abas acessíveis, botão SVG para copiar e um accordion fechado por padrão:
import { CodeBlock } from "safira-ui/react";
<CodeBlock
html={'<button class="sf-button">Salvar</button>'}
ts={`import { Button } from "safira-ui/react";
<Button>Salvar</Button>`}
/>;Use defaultLanguage="ts" para iniciar em TypeScript, terminal="npm install safira-ui" para comandos, defaultExpanded para começar aberto e animationDuration={300} para ajustar a transição. Defina collapsible={false} quando o código deve permanecer visível sem accordion. Os eventos onCodeCopy e onLanguageChange permitem integrar métricas ou feedback próprios. O onCopy nativo continua disponível. Se somente um formato for informado, o componente mostra apenas aquele formato.
Grid responsiva
Use colunas fixas para estruturas controladas ou minItemWidth para permitir que o CSS escolha quantos itens cabem em cada linha:
import { Card, Grid } from "safira-ui/react";
<Grid columns={3} minItemWidth="14rem" gap={4}>
<Card>Primeiro</Card>
<Card>Segundo</Card>
<Card>Terceiro</Card>
</Grid>;Quando minItemWidth é informado, a Grid usa auto-fit e se adapta sem JavaScript. columnGap e rowGap podem sobrescrever o gap em cada eixo. Em HTML puro, use sf-grid, data-fluid="true" e a variável --sf-grid-min-item.
Imagens nítidas e responsivas
Image torna alt obrigatório e gera um srcSet a partir de fontes reais. O navegador escolhe a resolução apropriada para o tamanho renderizado e para a densidade da tela:
import { Image } from "safira-ui/react";
<Image
src="foto-640.webp"
alt="Equipe reunida"
sources={[
{ src: "foto-640.webp", width: 640 },
{ src: "foto-1280.webp", width: 1280 },
]}
sizes="(max-width: 40rem) 100vw, 40rem"
aspectRatio="16 / 9"
fit="cover"
/>;Para o caso simples de uma imagem 2x, use highResolutionSrc. A Safira não inventa detalhes ausentes em um arquivo pequeno: a melhoria de nitidez acontece porque o componente entrega ao browser uma fonte maior. loading="lazy" e decoding="async" são os defaults e podem ser sobrescritos para imagens prioritárias.
Modal nativo e acessível
Modal utiliza <dialog> e mantém abertura modal, foco, Escape e retorno ao elemento acionador sob responsabilidade do browser. O título é obrigatório para garantir um nome acessível:
import { Button, Modal } from "safira-ui/react";
<Modal
id="confirmar-publicacao"
label="Publicar"
title="Confirmar publicação"
description="O conteúdo ficará visível para todas as pessoas."
actions={<Button>Confirmar</Button>}
onOpenChange={(open) => console.log(open)}
>
Revise as informações antes de continuar.
</Modal>;Use open para controle externo, defaultOpen para iniciar aberto e closeOnBackdrop={false} quando o fluxo exigir uma decisão explícita. className e styles aceitam as partes trigger, dialog, header, title, description, content, actions e close.
Em HTML, a abertura pode permanecer declarativa com commandfor e command="show-modal"; o fechamento funciona com um formulário method="dialog".
Personalização com className, Tailwind e unstyled
As variantes da Safira fornecem identidade visual, não bloqueiam o consumidor. Os wrappers preservam className, style, id, atributos data-*, eventos e demais atributos nativos:
<Button id="salvar-perfil" className="rounded-full bg-violet-600 px-8 hover:bg-violet-700" data-origin="perfil">
Salvar
</Button>Quando a aparência da Safira não for desejada, use unstyled. Essa variante remove fundo, borda, raio, peso, tamanho mínimo, padding e transições visuais, mas preserva o elemento nativo, foco visível, estado desabilitado, loading e composição dos ícones:
<Button variant="unstyled" className="rounded-md bg-emerald-600 px-4 py-2 font-semibold text-white">
Publicar
</Button>Em HTML, o mesmo contrato usa data-variant="unstyled":
<button class="sf-button minha-acao" data-variant="unstyled">Publicar</button>As classes do consumidor vencem por cascade quando o CSS da Safira é carregado primeiro. Propriedades inline em style sempre têm prioridade. Defaults semânticos e guardrails acessíveis continuam intencionais: por exemplo, Button usa type="button" por padrão e loading mantém o controle desabilitado e expõe aria-busy.
Posicionamento do Popover
Informe o lado preferido com placement. A Safira usa CSS Anchor Positioning para manter o popover junto ao botão e permite que o browser inverta o lado quando o conteúdo ultrapassaria a área visível:
<Popover id="acoes-do-perfil" label="Abrir ações" placement="bottom" title="Ações do perfil">
Conteúdo do popover
</Popover>Os valores aceitos são top, right, bottom e left. O valor é uma preferência, não uma posição rígida: por exemplo, um popover configurado como bottom poderá abrir acima quando estiver próximo ao final da viewport.
Em HTML puro, use o mesmo contrato declarativo:
<button class="sf-button" popovertarget="acoes">Abrir ações</button>
<div id="acoes" class="sf-popover" data-placement="bottom" popover>Conteúdo do popover</div>Progressive UI
A ordem de decisão da Safira é simples:
- HTML nativo para estrutura, semântica e comportamento.
- CSS para aparência, layout, estados e movimento.
- JavaScript apenas quando a plataforma não entrega a interação necessária.
- React como conveniência opcional, nunca como fundação visual.
Isso mantém o CSS utilizável em qualquer stack e evita enviar comportamento duplicado ao browser.
Temas e tokens
O site também possui um guia dedicado na rota /theme, com configuração para React, Next.js e Vite.
Ative o tema escuro com um atributo:
<html data-theme="dark"></html>Todos os tokens públicos usam custom properties --sf-*. Personalize-os globalmente ou dentro de um escopo:
.minha-marca {
--sf-color-primary: #6d28d9;
--sf-color-primary-hover: #5b21b6;
--sf-radius-md: 0.25rem;
--sf-font-sans: "Minha Fonte", system-ui, sans-serif;
}Acessibilidade
A Safira inclui no núcleo:
- navegação completa por teclado;
- foco visível com
:focus-visible; - nomes, descrições e erros associados aos controles;
- suporte a
prefers-reduced-motion; - estilos para
forced-colorse alto contraste; - estados importantes que não dependem apenas de cor;
- controles com áreas de interação confortáveis;
- HTML semântico antes de ARIA customizado.
A biblioteca fornece uma base segura, mas o produto final ainda precisa cuidar da ordem dos headings, textos, nomes das ações, fluxo de foco e testes com pessoas e tecnologias assistivas.
Documentação legível por pessoas e IA
O site foi estruturado para reduzir contexto implícito:
- cada página possui um propósito e um
h1claro; - cada componente apresenta finalidade, elemento nativo, exemplo HTML, exemplo TypeScript e propriedades;
- código permanece como texto real e copiável;
- metadados descrevem pacote, versão, licença e entrypoints;
llms.txtoferece um resumo estático para agentes e indexadores;visual-identity.mdregistra voz, semântica visual e critérios editoriais.
Conteúdo IA-friendly não substitui acessibilidade nem revisão humana. A mesma informação precisa continuar compreensível por teclado, leitor de tela, zoom, texto ampliado e leitura linear.
Estrutura do projeto
safira-ui/
├── src/
│ ├── styles/index.css # tokens, reset e componentes CSS
│ ├── react/ # componentes com implementação, tipos e testes colocados juntos
│ └── test/ # configuração dos testes
├── docs/ # documentação e laboratório em Vite
├── scripts/ # tarefas de build reproduzíveis
├── dist/ # pacote compilado, gerado pelo build
└── site-dist/ # site estático, gerado pelo buildDesenvolvimento
Requer Node.js 20.19 ou superior.
npm install
npm run devO servidor local abre a documentação e o laboratório de componentes.
Antes de enviar uma alteração, execute:
npm run checkEsse comando valida lint, TypeScript, testes, pacote da biblioteca e site da documentação.
Scripts úteis
| Comando | Resultado |
| -------------------- | ----------------------------------------------------------- |
| npm run dev | Inicia a documentação em modo de desenvolvimento. |
| npm run lint | Verifica as regras estáticas. |
| npm run typecheck | Valida os tipos sem emitir arquivos. |
| npm test | Executa os testes com Vitest. |
| npm run build | Gera CSS, ESM, CommonJS e declarações. |
| npm run build:docs | Gera o site estático em site-dist. |
| npm run check | Executa toda a esteira de qualidade. |
| npm run 1 | Abre o login web do npm. |
| npm run 2 | Confirma qual conta npm está autenticada. |
| npm run 3 | Valida o projeto e simula o conteúdo do pacote. |
| npm run 4 | Publica no npm a versão atual registrada no package.json. |
Publicação no npm
Atualize a versão, revise as alterações e faça o commit antes da publicação. Depois execute os quatro passos em ordem:
npm run 1
npm run 2
npm run 3
npm run 4O terceiro passo executa a esteira completa e mostra exatamente quais arquivos entrarão no pacote. Confira nome, versão e conteúdo antes de seguir para a publicação. Códigos de autenticação devem ser informados diretamente no terminal, nunca em chats ou arquivos.
Para publicar uma versão de pré-lançamento com outra tag:
npm run 4 -- --tag nextSaída do pacote
dist/safira.css— núcleo visual estático;dist/react.js— wrappers React em ESM;dist/react.cjs— wrappers React em CommonJS;dist/**/*.d.ts— declarações TypeScript.
Contribuição
Leia AGENTS.md antes de alterar componentes, tokens ou configuração de build. Para conteúdo, campanhas e documentação, preserve também as decisões de visual-identity.md.
Licença
Distribuído sob a licença MIT.
CSS primeiro. Pessoas sempre.
