React & Frontend

Headless Components em React: Lógica sem Apresentação com Radix UI na Prática

20 min de leitura

Headless Components em React: Lógica sem Apresentação com Radix UI na Prática

O Que São Headless Components? Um headless component é um componente React que encapsula toda a lógica, estado e comportamento de um elemento de interface, mas deliberadamente não fornece nenhuma marcação HTML ou estilo visual. Essa separação entre a inteligência (lógica) e a apresentação (visual) permite que você reutilize a mesma lógica complexa em múltiplos contextos visuais diferentes, sem duplicar código. A ideia central é inverter o modelo tradicional de desenvolvimento de componentes. Em vez de criar um componente monolítico que traz consigo HTML, CSS e JavaScript acoplados, você trabalha com dois planos distintos: o componente sem cabeça, que gerencia tudo relacionado ao comportamento (acessibilidade, keyboard navigation, estado, focus management), e o componente apresentacional, que você constrói para refletir a identidade visual do seu projeto. Isso não é uma abstração teórica — é um padrão pragmático que bibliotecas como Radix UI exploram para resolver problemas reais de acessibilidade e reutilização. Introdução ao Radix UI O que é Radix UI e

<h2>O Que São Headless Components?</h2>

<p>Um headless component é um componente React que encapsula toda a lógica, estado e comportamento de um elemento de interface, mas deliberadamente não fornece nenhuma marcação HTML ou estilo visual. Essa separação entre a inteligência (lógica) e a apresentação (visual) permite que você reutilize a mesma lógica complexa em múltiplos contextos visuais diferentes, sem duplicar código.</p>

<p>A ideia central é inverter o modelo tradicional de desenvolvimento de componentes. Em vez de criar um componente monolítico que traz consigo HTML, CSS e JavaScript acoplados, você trabalha com dois planos distintos: o componente sem cabeça, que gerencia tudo relacionado ao comportamento (acessibilidade, keyboard navigation, estado, focus management), e o componente apresentacional, que você constrói para refletir a identidade visual do seu projeto. Isso não é uma abstração teórica — é um padrão pragmático que bibliotecas como Radix UI exploram para resolver problemas reais de acessibilidade e reutilização.</p>

<h2>Introdução ao Radix UI</h2>

<h3>O que é Radix UI e por que escolher?</h3>

<p>Radix UI é uma biblioteca de componentes headless de baixo nível, mantida pela comunidade React, que oferece primitivos acessíveis prontos para uso. Ela não impõe nenhuma opinião visual — você controla 100% do CSS e da marcação — mas fornece toda a engenharia pesada: ARIA attributes corretos, keyboard interactions (setas, Enter, Escape), focus trap em modais, gerenciamento de estado complexo e muito mais.</p>

<p>Escolher Radix UI significa escolher acessibilidade de primeira classe, sem a rigidez visual de bibliotecas opintonadas como Material-UI ou Chakra UI. Você obtém um contrato explícito: a lógica é controlada por Radix, a apresentação é sua responsabilidade. Isso resulta em designs únicos, leve como ar, sem CSS bloated.</p>

<h3>Instalação e Configuração Inicial</h3>

<p>Para começar, você precisa apenas instalar o pacote do componente que vai usar. Vamos iniciar com um simples exemplo usando <code>@radix-ui/react-dialog</code>:</p>

<pre><code class="language-bash">npm install @radix-ui/react-dialog</code></pre>

<p>Se você estiver usando TypeScript (recomendado), os tipos já vêm inclusos. A configuração é mínima — não há providers globais ou temas para configurar. Você importa, usa, e controla tudo o mais.</p>

<h2>Criando Seu Primeiro Headless Component com Radix UI</h2>

<h3>Exemplo Prático: Dialog (Modal) Personalizado</h3>

<p>Vamos construir um diálogo modal do zero usando Radix UI. Este exemplo mostrará como o Radix fornece a lógica, e você fornece a apresentação:</p>

<pre><code class="language-jsx">import React, { useState } from &#039;react&#039;;

import * as Dialog from &#039;@radix-ui/react-dialog&#039;;

import styles from &#039;./CustomDialog.module.css&#039;;

export function CustomDialog() {

const [open, setOpen] = useState(false);

return (

&lt;Dialog.Root open={open} onOpenChange={setOpen}&gt;

{/ O trigger é apenas um botão. Radix não impõe nada aqui /}

&lt;Dialog.Trigger asChild&gt;

&lt;button className={styles.triggerButton}&gt;

Abrir Diálogo

&lt;/button&gt;

&lt;/Dialog.Trigger&gt;

{/ Portal renderiza o conteúdo em um div fora da hierarquia /}

&lt;Dialog.Portal&gt;

{/ Overlay é o fundo escuro. Você controla o estilo completamente /}

&lt;Dialog.Overlay className={styles.overlay} /&gt;

{/ Content é o diálogo em si. Radix gerencia focus, keyboard escape, etc /}

&lt;Dialog.Content className={styles.content}&gt;

&lt;div className={styles.header}&gt;

&lt;Dialog.Title className={styles.title}&gt;

Confirme sua ação

&lt;/Dialog.Title&gt;

&lt;Dialog.Close asChild&gt;

&lt;button

className={styles.closeButton}

aria-label=&quot;Fechar diálogo&quot;

&gt;

&lt;/button&gt;

&lt;/Dialog.Close&gt;

&lt;/div&gt;

&lt;Dialog.Description className={styles.description}&gt;

Esta é uma descrição do diálogo. Radix já adicionou os atributos

ARIA necessários automaticamente.

&lt;/Dialog.Description&gt;

&lt;div className={styles.footer}&gt;

&lt;Dialog.Close asChild&gt;

&lt;button className={styles.buttonSecondary}&gt;

Cancelar

&lt;/button&gt;

&lt;/Dialog.Close&gt;

&lt;button className={styles.buttonPrimary}&gt;

Confirmar

&lt;/button&gt;

&lt;/div&gt;

&lt;/Dialog.Content&gt;

&lt;/Dialog.Portal&gt;

&lt;/Dialog.Root&gt;

);

}</code></pre>

<p>Agora o CSS do seu módulo, que demonstra a liberdade visual que você tem:</p>

<pre><code class="language-css">/ CustomDialog.module.css /

.triggerButton {

padding: 10px 16px;

background: #0066cc;

color: white;

border: none;

border-radius: 6px;

cursor: pointer;

font-weight: 500;

font-size: 14px;

}

.triggerButton:hover {

background: #0052a3;

}

.overlay {

background: rgba(0, 0, 0, 0.5);

position: fixed;

inset: 0;

animation: fadeIn 150ms ease-out;

}

@keyframes fadeIn {

from {

opacity: 0;

}

to {

opacity: 1;

}

}

.content {

background: white;

border-radius: 12px;

box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);

position: fixed;

left: 50%;

top: 50%;

transform: translate(-50%, -50%);

width: 90vw;

max-width: 500px;

padding: 24px;

animation: slideIn 200ms cubic-bezier(0.16, 1, 0.3, 1);

}

@keyframes slideIn {

from {

opacity: 0;

transform: translate(-50%, -48%);

}

to {

opacity: 1;

transform: translate(-50%, -50%);

}

}

.header {

display: flex;

justify-content: space-between;

align-items: center;

margin-bottom: 12px;

}

.title {

margin: 0;

font-size: 18px;

font-weight: 600;

color: #1a1a1a;

}

.closeButton {

background: none;

border: none;

font-size: 24px;

cursor: pointer;

color: #666;

padding: 4px;

display: flex;

align-items: center;

justify-content: center;

}

.closeButton:hover {

color: #000;

}

.description {

margin: 16px 0;

color: #555;

line-height: 1.5;

font-size: 14px;

}

.footer {

display: flex;

gap: 12px;

justify-content: flex-end;

margin-top: 20px;

}

.buttonSecondary {

padding: 8px 16px;

background: #f0f0f0;

color: #1a1a1a;

border: none;

border-radius: 6px;

cursor: pointer;

font-weight: 500;

font-size: 14px;

}

.buttonSecondary:hover {

background: #e0e0e0;

}

.buttonPrimary {

padding: 8px 16px;

background: #0066cc;

color: white;

border: none;

border-radius: 6px;

cursor: pointer;

font-weight: 500;

font-size: 14px;

}

.buttonPrimary:hover {

background: #0052a3;

}</code></pre>

<p>Aqui está o ponto crucial: Radix UI forneceu toda a orquestração — fechar quando você pressiona Escape, renderizar no portal, gerenciar focus, adicionar <code>role=&quot;dialog&quot;</code> e <code>aria-labelledby</code> automaticamente. Você forneceu apenas o visual. Se quisesse um design completamente diferente, seria como trocar o CSS — a lógica permaneceria exatamente a igual.</p>

<h3>Entendendo os Componentes Compostos</h3>

<p>Radix UI trabalha com o padrão de composição. Cada primitivo é uma coleção de sub-componentes que você monta junto. No exemplo anterior, usamos <code>Dialog.Root</code>, <code>Dialog.Trigger</code>, <code>Dialog.Portal</code>, <code>Dialog.Overlay</code> e <code>Dialog.Content</code>. Cada um tem responsabilidades específicas:</p>

<ul>

<li><strong>Root</strong>: gerencia o estado aberto/fechado do diálogo inteiro</li>

<li><strong>Trigger</strong>: o botão que abre (pode ser qualquer elemento com <code>asChild</code>)</li>

<li><strong>Portal</strong>: renderiza em um portal para evitar problemas de z-index</li>

<li><strong>Overlay</strong>: o fundo semitransparente</li>

<li><strong>Content</strong>: o diálogo em si, onde a lógica de focus e keyboard é aplicada</li>

</ul>

<p>Você não precisa usar todos se não quiser. Se não precisar de um overlay, simplesmente não inclua. Se quiser customizar o trigger, use <code>asChild</code> para passar seu próprio elemento.</p>

<h2>Padrões Avançados com Headless Components</h2>

<h3>Criando um Select Customizado</h3>

<p>Selectores são logicamente complexos. Radix UI oferece <code>@radix-ui/react-select</code> que encapsula toda essa complexidade. Vamos usá-lo:</p>

<pre><code class="language-bash">npm install @radix-ui/react-select @radix-ui/react-icons</code></pre>

<pre><code class="language-jsx">import React from &#039;react&#039;;

import * as Select from &#039;@radix-ui/react-select&#039;;

import { CheckIcon, ChevronDownIcon } from &#039;@radix-ui/react-icons&#039;;

import styles from &#039;./CustomSelect.module.css&#039;;

export function CustomSelect() {

const [value, setValue] = React.useState(&#039;option-1&#039;);

return (

&lt;Select.Root value={value} onValueChange={setValue}&gt;

{/ Trigger: o botão que abre o select /}

&lt;Select.Trigger className={styles.trigger}&gt;

&lt;Select.Value placeholder=&quot;Escolha uma opção&quot; /&gt;

&lt;Select.Icon&gt;

&lt;ChevronDownIcon /&gt;

&lt;/Select.Icon&gt;

&lt;/Select.Trigger&gt;

{/ Portal e Content: contêm as opções /}

&lt;Select.Portal&gt;

&lt;Select.Content className={styles.content}&gt;

{/ ScrollUpButton aparece quando há scroll /}

&lt;Select.ScrollUpButton className={styles.scrollButton}&gt;

&lt;/Select.ScrollUpButton&gt;

{/ Viewport contém as opções /}

&lt;Select.Viewport className={styles.viewport}&gt;

{/ Grupo de opções relacionadas /}

&lt;Select.Group&gt;

&lt;Select.Label className={styles.label}&gt;

Frutas

&lt;/Select.Label&gt;

&lt;Select.Item value=&quot;option-1&quot; className={styles.item}&gt;

&lt;Select.ItemText&gt;Maçã&lt;/Select.ItemText&gt;

&lt;Select.ItemIndicator&gt;

&lt;CheckIcon /&gt;

&lt;/Select.ItemIndicator&gt;

&lt;/Select.Item&gt;

&lt;Select.Item value=&quot;option-2&quot; className={styles.item}&gt;

&lt;Select.ItemText&gt;Banana&lt;/Select.ItemText&gt;

&lt;Select.ItemIndicator&gt;

&lt;CheckIcon /&gt;

&lt;/Select.ItemIndicator&gt;

&lt;/Select.Item&gt;

&lt;Select.Item value=&quot;option-3&quot; className={styles.item}&gt;

&lt;Select.ItemText&gt;Laranja&lt;/Select.ItemText&gt;

&lt;Select.ItemIndicator&gt;

&lt;CheckIcon /&gt;

&lt;/Select.ItemIndicator&gt;

&lt;/Select.Item&gt;

&lt;/Select.Group&gt;

&lt;Select.Separator className={styles.separator} /&gt;

&lt;Select.Group&gt;

&lt;Select.Label className={styles.label}&gt;

Vegetais

&lt;/Select.Label&gt;

&lt;Select.Item value=&quot;option-4&quot; className={styles.item}&gt;

&lt;Select.ItemText&gt;Cenoura&lt;/Select.ItemText&gt;

&lt;Select.ItemIndicator&gt;

&lt;CheckIcon /&gt;

&lt;/Select.ItemIndicator&gt;

&lt;/Select.Item&gt;

&lt;Select.Item value=&quot;option-5&quot; className={styles.item}&gt;

&lt;Select.ItemText&gt;Brócolis&lt;/Select.ItemText&gt;

&lt;Select.ItemIndicator&gt;

&lt;CheckIcon /&gt;

&lt;/Select.ItemIndicator&gt;

&lt;/Select.Item&gt;

&lt;/Select.Group&gt;

&lt;/Select.Viewport&gt;

&lt;Select.ScrollDownButton className={styles.scrollButton}&gt;

&lt;/Select.ScrollDownButton&gt;

&lt;/Select.Content&gt;

&lt;/Select.Portal&gt;

&lt;/Select.Root&gt;

);

}</code></pre>

<p>CSS para o Select customizado:</p>

<pre><code class="language-css">/ CustomSelect.module.css /

.trigger {

display: flex;

align-items: center;

justify-content: space-between;

padding: 10px 12px;

background: white;

border: 1px solid #ccc;

border-radius: 6px;

cursor: pointer;

font-size: 14px;

min-width: 200px;

transition: border-color 200ms;

}

.trigger:hover {

border-color: #999;

}

.trigger:focus {

outline: none;

border-color: #0066cc;

box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);

}

.content {

background: white;

border: 1px solid #ccc;

border-radius: 6px;

box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);

z-index: 1000;

animation: slideDown 200ms ease-out;

}

@keyframes slideDown {

from {

opacity: 0;

transform: translateY(-10px);

}

to {

opacity: 1;

transform: translateY(0);

}

}

.viewport {

padding: 4px 0;

max-height: 300px;

overflow: auto;

}

.scrollButton {

display: flex;

align-items: center;

justify-content: center;

height: 24px;

background: #f5f5f5;

color: #666;

cursor: pointer;

font-size: 12px;

}

.scrollButton:hover {

background: #e8e8e8;

}

.label {

padding: 8px 12px;

font-size: 12px;

font-weight: 600;

color: #999;

text-transform: uppercase;

letter-spacing: 0.5px;

}

.item {

padding: 8px 12px;

display: flex;

align-items: center;

justify-content: space-between;

cursor: pointer;

font-size: 14px;

color: #1a1a1a;

transition: background-color 100ms;

user-select: none;

}

.item:hover {

background: #f0f0f0;

}

.item[data-state=&#039;checked&#039;] {

background: #e8f0ff;

color: #0066cc;

}

.item[data-highlighted] {

background: #f5f5f5;

}

.separator {

height: 1px;

background: #e0e0e0;

margin: 4px 0;

}</code></pre>

<p>Observe que você usou <code>data-state</code> e <code>data-highlighted</code> no CSS. Radix adiciona esses atributos automaticamente — você os aproveita para estilizar sem adicionar lógica extra em JavaScript. A biblioteca gerencia navegação por teclado (setas, Home, End, Type-ahead), acessibilidade ARIA e tudo mais.</p>

<h3>Composição e Reutilização de Lógica</h3>

<p>O verdadeiro poder emerge quando você encapsula um headless component em sua própria abstração. Vamos criar um wrapper que reutiliza o Select em múltiplos contextos com visuais diferentes:</p>

<pre><code class="language-jsx">// useSelectLogic.js - Hook customizado que encapsula lógica de negócio

export function useSelectLogic(items) {

const [selectedId, setSelectedId] = React.useState(items[0]?.id);

const selectedItem = items.find(item =&gt; item.id === selectedId);

return {

selectedId,

setSelectedId,

selectedItem,

items

};

}

// SelectCompact.jsx - Versão compacta do Select

import * as Select from &#039;@radix-ui/react-select&#039;;

import styles from &#039;./SelectCompact.module.css&#039;;

export function SelectCompact({ items, value, onChange }) {

return (

&lt;Select.Root value={value} onValueChange={onChange}&gt;

&lt;Select.Trigger className={styles.compactTrigger}&gt;

&lt;Select.Value /&gt;

&lt;Select.Icon&gt;▼&lt;/Select.Icon&gt;

&lt;/Select.Trigger&gt;

&lt;Select.Portal&gt;

&lt;Select.Content className={styles.compactContent}&gt;

&lt;Select.Viewport className={styles.compactViewport}&gt;

{items.map(item =&gt; (

&lt;Select.Item

key={item.id}

value={item.id}

className={styles.compactItem}

&gt;

&lt;Select.ItemText&gt;{item.label}&lt;/Select.ItemText&gt;

&lt;/Select.Item&gt;

))}

&lt;/Select.Viewport&gt;

&lt;/Select.Content&gt;

&lt;/Select.Portal&gt;

&lt;/Select.Root&gt;

);

}

// SelectFull.jsx - Versão completa com detalhes

import * as Select from &#039;@radix-ui/react-select&#039;;

import styles from &#039;./SelectFull.module.css&#039;;

export function SelectFull({ items, value, onChange }) {

return (

&lt;Select.Root value={value} onValueChange={onChange}&gt;

&lt;Select.Trigger className={styles.fullTrigger}&gt;

&lt;Select.Value placeholder=&quot;Selecione um item&quot; /&gt;

&lt;Select.Icon&gt;⌄&lt;/Select.Icon&gt;

&lt;/Select.Trigger&gt;

&lt;Select.Portal&gt;

&lt;Select.Content className={styles.fullContent}&gt;

&lt;Select.ScrollUpButton&gt;⬆&lt;/Select.ScrollUpButton&gt;

&lt;Select.Viewport className={styles.fullViewport}&gt;

{items.map(item =&gt; (

&lt;Select.Item

key={item.id}

value={item.id}

className={styles.fullItem}

&gt;

&lt;Select.ItemText&gt;

&lt;div className={styles.itemContent}&gt;

&lt;span className={styles.itemLabel}&gt;{item.label}&lt;/span&gt;

{item.description &amp;&amp; (

&lt;span className={styles.itemDescription}&gt;

{item.description}

&lt;/span&gt;

)}

&lt;/div&gt;

&lt;/Select.ItemText&gt;

&lt;/Select.Item&gt;

))}

&lt;/Select.Viewport&gt;

&lt;Select.ScrollDownButton&gt;⬇&lt;/Select.ScrollDownButton&gt;

&lt;/Select.Content&gt;

&lt;/Select.Portal&gt;

&lt;/Select.Root&gt;

);

}

// App.jsx - Utilizando ambas as variações com a mesma lógica

import { useSelectLogic } from &#039;./useSelectLogic&#039;;

import { SelectCompact } from &#039;./SelectCompact&#039;;

import { SelectFull } from &#039;./SelectFull&#039;;

const PRODUCTS = [

{ id: &#039;1&#039;, label: &#039;Notebook&#039; },

{ id: &#039;2&#039;, label: &#039;Mouse&#039;, description: &#039;Sem fio&#039; },

{ id: &#039;3&#039;, label: &#039;Teclado&#039;, description: &#039;Mecânico RGB&#039; }

];

export function App() {

const { selectedId, setSelectedId, items } = useSelectLogic(PRODUCTS);

return (

&lt;div&gt;

&lt;h2&gt;Versão Compacta&lt;/h2&gt;

&lt;SelectCompact

items={items}

value={selectedId}

onChange={setSelectedId}

/&gt;

&lt;h2&gt;Versão Completa&lt;/h2&gt;

&lt;SelectFull

items={items}

value={selectedId}

onChange={setSelectedId}

/&gt;

&lt;/div&gt;

);

}</code></pre>

<p>Aqui você viu o padrão em ação: a mesma lógica (<code>useSelectLogic</code>) alimenta dois componentes visuais completamente diferentes (<code>SelectCompact</code> e <code>SelectFull</code>). Ambos usam Radix UI para a orquestração, mas você controla tudo o mais. Se precisasse de uma terceira variação, seria trivial.</p>

<h2>Acessibilidade como Padrão</h2>

<h3>Por Que Radix UI Torna Acessibilidade Fácil</h3>

<p>Acessibilidade é frequentemente negligenciada porque é chata de implementar manualmente. Você precisa se lembrar de adicionar <code>role=&quot;dialog&quot;</code>, <code>aria-labelledby</code>, <code>aria-describedby</code>, gerenciar focus trap, implementar keyboard navigation, testar com leitores de tela... Radix UI faz tudo isso para você por padrão.</p>

<p>Quando você usa <code>Dialog.Content</code> do Radix, ele automaticamente:</p>

<ul>

<li>Adiciona <code>role=&quot;dialog&quot;</code></li>

<li>Conecta <code>aria-labelledby</code> ao seu <code>Dialog.Title</code></li>

<li>Conecta <code>aria-describedby</code> ao seu <code>Dialog.Description</code></li>

<li>Implementa focus trap (focus não sai do diálogo enquanto está aberto)</li>

<li>Permite fechar com Escape</li>

<li>Restaura focus ao elemento que abriu o diálogo</li>

</ul>

<p>Você não escreveu uma linha de JavaScript para tudo isso. Verificar a acessibilidade se torna simples — você já começa correto.</p>

<p>Exemplo com <code>@radix-ui/react-tabs</code>:</p>

<pre><code class="language-jsx">import React from &#039;react&#039;;

import * as Tabs from &#039;@radix-ui/react-tabs&#039;;

import styles from &#039;./AccessibleTabs.module.css&#039;;

export function AccessibleTabs() {

return (

&lt;Tabs.Root defaultValue=&quot;tab1&quot; className={styles.tabsRoot}&gt;

{/ Radix adiciona role=&quot;tablist&quot; automaticamente /}

&lt;Tabs.List className={styles.tabsList}&gt;

{/ Radix adiciona role=&quot;tab&quot; e aria-selected automaticamente /}

&lt;Tabs.Trigger value=&quot;tab1&quot; className={styles.trigger}&gt;

Descrição

&lt;/Tabs.Trigger&gt;

&lt;Tabs.Trigger value=&quot;tab2&quot; className={styles.trigger}&gt;

Especificações

&lt;/Tabs.Trigger&gt;

&lt;Tabs.Trigger value=&quot;tab3&quot; className={styles.trigger}&gt;

Avaliações

&lt;/Tabs.Trigger&gt;

&lt;/Tabs.List&gt;

{/ Radix adiciona role=&quot;tabpanel&quot; automaticamente /}

&lt;Tabs.Content value=&quot;tab1&quot; className={styles.content}&gt;

&lt;p&gt;Esta é a descrição do produto. Radix mantém este conteúdo no DOM mas o oculta do leitor de tela quando a aba não está ativa.&lt;/p&gt;

&lt;/Tabs.Content&gt;

&lt;Tabs.Content value=&quot;tab2&quot; className={styles.content}&gt;

&lt;ul&gt;

&lt;li&gt;Material: Plástico de alta resistência&lt;/li&gt;

&lt;li&gt;Dimensões: 10cm x 10cm x 5cm&lt;/li&gt;

&lt;li&gt;Peso: 250g&lt;/li&gt;

&lt;/ul&gt;

&lt;/Tabs.Content&gt;

&lt;Tabs.Content value=&quot;tab3&quot; className={styles.content}&gt;

&lt;p&gt;⭐⭐⭐⭐⭐ Excelente produto!&lt;/p&gt;

&lt;/Tabs.Content&gt;

&lt;/Tabs.Root&gt;

);

}</code></pre>

<pre><code class="language-css">.tabsRoot {

display: flex;

flex-direction: column;

width: 100%;

}

.tabsList {

display: flex;

border-bottom: 2px solid #e0e0e0;

}

.trigger {

padding: 12px 16px;

background: none;

border: none;

cursor: pointer;

font-weight: 500;

color: #666;

border-bottom: 2px solid transparent;

margin-bottom: -2px;

transition: all 200ms;

}

.trigger:hover {

color: #333;

}

.trigger[data-state=&#039;active&#039;] {

color: #0066cc;

border-bottom-color: #0066cc;

}

.content {

padding: 16px 0;

line-height: 1.6;

}</code></pre>

<p>Observe que você nem pensou em ARIA — Radix cuidou. Navegue com Tab, depois com as setas esquerda/direita e tudo funciona perfeitamente para usuários de teclado e leitores de tela.</p>

<h2>Conclusão</h2>

<p>Aprendemos que headless components revolucionam como você constrói interfaces ao separar inteligência de apresentação. Radix UI oferece uma implementação madura dessa filosofia, fornecendo lógica e acessibilidade sem impor nenhum visual — você fica com a liberdade de design e a tranquilidade de acessibilidade garantida desde o início. O padrão de composição permite reutilizar a mesma lógica em múltiplos contextos visuais, reduzindo drasticamente duplicação de código e facilitando manutenção.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.radix-ui.com" target="_blank" rel="noopener noreferrer">Radix UI Official Documentation</a></li>

<li><a href="https://www.radix-ui.com/docs/primitives/components/dialog" target="_blank" rel="noopener noreferrer">Radix UI Dialog Component</a></li>

<li><a href="https://www.w3.org/WAI/ARIA/apg/" target="_blank" rel="noopener noreferrer">WAI-ARIA Authoring Practices Guide</a></li>

<li><a href="https://reach.tech/" target="_blank" rel="noopener noreferrer">Reach UI - Accessible Components for React</a></li>

<li><a href="https://www.smashingmagazine.com/2022/05/building-composable-accessible-web-components/" target="_blank" rel="noopener noreferrer">Headless Components: A Pattern for Composable User Interfaces</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Como Usar Feature Flags em React: LaunchDarkly, Unleash e Rollouts Graduais em Produção
Como Usar Feature Flags em React: LaunchDarkly, Unleash e Rollouts Graduais em Produção

O que são Feature Flags e por que eles importam Feature flags (ou feature tog...

O que Todo Dev Deve Saber sobre React Server Components: Modelo Mental e Casos de Uso Reais
O que Todo Dev Deve Saber sobre React Server Components: Modelo Mental e Casos de Uso Reais

O que são React Server Components? React Server Components (RSCs) representam...

Boas Práticas de Testing Library em Profundidade: Queries, Fire Events e Async para Times Ágeis
Boas Práticas de Testing Library em Profundidade: Queries, Fire Events e Async para Times Ágeis

Testing Library em Profundidade: Queries, Fire Events e Async Testing Library...