React & Frontend

O que Todo Dev Deve Saber sobre Design System com React: Tokens, Variantes e Acessibilidade

16 min de leitura

O que Todo Dev Deve Saber sobre Design System com React: Tokens, Variantes e Acessibilidade

O Que é um Design System e Por Que Usá-lo Um Design System é um conjunto de componentes, tokens e diretrizes que garantem consistência visual e funcional em toda uma aplicação. Ao invés de cada desenvolvedor criar seu próprio estilo de botão, input ou card, você centraliza essas decisões em uma fonte única de verdade. Isso não é apenas sobre padronização estética — é sobre escalabilidade, manutenção e experiência do usuário. No contexto de React, um Design System bem construído reduz drasticamente o tempo de desenvolvimento, diminui bugs relacionados a acessibilidade e facilita mudanças globais de branding. Quando você precisa alterar a cor primária do projeto inteiro, basta atualizar um token em um único lugar, e a mudança propaga-se automaticamente por todos os componentes que o utilizam. Por Que React é Ideal para Design Systems React oferece abstrações perfeitas para construir sistemas reutilizáveis através de componentes. Cada botão, ícone ou card torna-se um bloco funcional isolado que pode receber props

<h2>O Que é um Design System e Por Que Usá-lo</h2>

<p>Um Design System é um conjunto de componentes, tokens e diretrizes que garantem consistência visual e funcional em toda uma aplicação. Ao invés de cada desenvolvedor criar seu próprio estilo de botão, input ou card, você centraliza essas decisões em uma fonte única de verdade. Isso não é apenas sobre padronização estética — é sobre escalabilidade, manutenção e experiência do usuário.</p>

<p>No contexto de React, um Design System bem construído reduz drasticamente o tempo de desenvolvimento, diminui bugs relacionados a acessibilidade e facilita mudanças globais de branding. Quando você precisa alterar a cor primária do projeto inteiro, basta atualizar um token em um único lugar, e a mudança propaga-se automaticamente por todos os componentes que o utilizam.</p>

<h3>Por Que React é Ideal para Design Systems</h3>

<p>React oferece abstrações perfeitas para construir sistemas reutilizáveis através de componentes. Cada botão, ícone ou card torna-se um bloco funcional isolado que pode receber props para se adaptar a diferentes contextos. A composição de componentes permite criar interfaces complexas a partir de peças simples, mantendo o código limpo e testável. Além disso, o ecossistema React tem ferramentas maduras como Storybook que facilitam a documentação e o desenvolvimento de componentes em isolamento.</p>

<h2>Design Tokens: A Fundação do Sistema</h2>

<p>Design Tokens são valores reutilizáveis que definem os aspectos visuais de um design: cores, tipografia, espaçamento, sombras e raios de borda. Ao invés de escrever <code>color: #3B82F6</code> diretamente no código, você define um token chamado <code>primaryColor</code> que conterá esse valor. Isso cria uma camada de abstração que facilita manutenção e garantir consistência.</p>

<p>Os tokens devem ser organizados hierarquicamente, começando pelos mais primitivos (cores base, tamanhos) até os compostos (espaçamento de um formulário, sombra de um card). Uma boa estrutura de tokens permite que designers e desenvolvedores falem a mesma linguagem.</p>

<h3>Estrutura e Implementação de Tokens</h3>

<p>Vamos criar uma estrutura de tokens bem organizada usando JavaScript puro, que pode ser facilmente integrada com CSS-in-JS ou Tailwind:</p>

<pre><code class="language-javascript">// tokens.js

const tokens = {

colors: {

// Cores primitivas

neutral: {

50: &#039;#F9FAFB&#039;,

100: &#039;#F3F4F6&#039;,

200: &#039;#E5E7EB&#039;,

300: &#039;#D1D5DB&#039;,

400: &#039;#9CA3AF&#039;,

500: &#039;#6B7280&#039;,

600: &#039;#4B5563&#039;,

700: &#039;#374151&#039;,

800: &#039;#1F2937&#039;,

900: &#039;#111827&#039;,

},

primary: {

50: &#039;#EFF6FF&#039;,

100: &#039;#DBEAFE&#039;,

500: &#039;#3B82F6&#039;,

600: &#039;#2563EB&#039;,

700: &#039;#1D4ED8&#039;,

900: &#039;#1E3A8A&#039;,

},

semantic: {

success: &#039;#10B981&#039;,

warning: &#039;#F59E0B&#039;,

error: &#039;#EF4444&#039;,

info: &#039;#0EA5E9&#039;,

},

},

typography: {

fontFamily: {

sans: &#039;-apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, sans-serif&#039;,

mono: &#039;Menlo, Monaco, Courier New, monospace&#039;,

},

fontSize: {

xs: &#039;0.75rem&#039;, // 12px

sm: &#039;0.875rem&#039;, // 14px

base: &#039;1rem&#039;, // 16px

lg: &#039;1.125rem&#039;, // 18px

xl: &#039;1.25rem&#039;, // 20px

&#039;2xl&#039;: &#039;1.5rem&#039;, // 24px

},

fontWeight: {

light: 300,

normal: 400,

medium: 500,

semibold: 600,

bold: 700,

},

lineHeight: {

tight: 1.2,

normal: 1.5,

relaxed: 1.75,

},

},

spacing: {

xs: &#039;0.25rem&#039;, // 4px

sm: &#039;0.5rem&#039;, // 8px

md: &#039;1rem&#039;, // 16px

lg: &#039;1.5rem&#039;, // 24px

xl: &#039;2rem&#039;, // 32px

&#039;2xl&#039;: &#039;3rem&#039;, // 48px

&#039;3xl&#039;: &#039;4rem&#039;, // 64px

},

borderRadius: {

none: &#039;0&#039;,

sm: &#039;0.25rem&#039;,

md: &#039;0.375rem&#039;,

lg: &#039;0.5rem&#039;,

xl: &#039;0.75rem&#039;,

full: &#039;9999px&#039;,

},

shadows: {

none: &#039;none&#039;,

sm: &#039;0 1px 2px 0 rgba(0, 0, 0, 0.05)&#039;,

md: &#039;0 4px 6px -1px rgba(0, 0, 0, 0.1)&#039;,

lg: &#039;0 10px 15px -3px rgba(0, 0, 0, 0.1)&#039;,

xl: &#039;0 20px 25px -5px rgba(0, 0, 0, 0.1)&#039;,

},

transitions: {

fast: &#039;150ms ease-in-out&#039;,

base: &#039;200ms ease-in-out&#039;,

slow: &#039;300ms ease-in-out&#039;,

},

};

export default tokens;</code></pre>

<p>Agora criamos um contexto React para distribuir esses tokens por toda a aplicação:</p>

<pre><code class="language-javascript">// ThemeProvider.jsx

import React, { createContext, useContext } from &#039;react&#039;;

import tokens from &#039;./tokens&#039;;

const ThemeContext = createContext(null);

export const ThemeProvider = ({ children }) =&gt; {

return (

&lt;ThemeContext.Provider value={tokens}&gt;

{children}

&lt;/ThemeContext.Provider&gt;

);

};

export const useTokens = () =&gt; {

const context = useContext(ThemeContext);

if (!context) {

throw new Error(&#039;useTokens deve ser usado dentro de ThemeProvider&#039;);

}

return context;

};</code></pre>

<h3>Usando Tokens em Componentes</h3>

<p>Com os tokens definidos, podemos criar componentes que os consomem de forma limpa:</p>

<pre><code class="language-javascript">// Button.jsx

import styled from &#039;styled-components&#039;;

import { useTokens } from &#039;./ThemeProvider&#039;;

const StyledButton = styled.button`

padding: ${props =&gt; props.tokens.spacing.md} ${props =&gt; props.tokens.spacing.lg};

font-size: ${props =&gt; props.tokens.typography.fontSize.base};

font-weight: ${props =&gt; props.tokens.typography.fontWeight.semibold};

border: none;

border-radius: ${props =&gt; props.tokens.borderRadius.md};

cursor: pointer;

transition: all ${props =&gt; props.tokens.transitions.base};

background-color: ${props =&gt; props.variant === &#039;primary&#039;

? props.tokens.colors.primary[600]

: props.tokens.colors.neutral[100]};

color: ${props =&gt; props.variant === &#039;primary&#039;

? &#039;#FFFFFF&#039;

: props.tokens.colors.neutral[900]};

&amp;:hover {

background-color: ${props =&gt; props.variant === &#039;primary&#039;

? props.tokens.colors.primary[700]

: props.tokens.colors.neutral[200]};

}

&amp;:disabled {

opacity: 0.6;

cursor: not-allowed;

}

`;

const Button = ({ children, variant = &#039;primary&#039;, ...props }) =&gt; {

const tokens = useTokens();

return (

&lt;StyledButton tokens={tokens} variant={variant} {...props}&gt;

{children}

&lt;/StyledButton&gt;

);

};

export default Button;</code></pre>

<h2>Variantes e Composição de Componentes</h2>

<p>Variantes são diferentes estados visuais de um componente que herdam a maioria das características base, mas diferem em aspecto específicos. Um botão pode ter variantes de tamanho (small, medium, large), tipo (primary, secondary, danger) e estado (normal, loading, disabled). A gestão inteligente de variantes evita duplicação de código e facilita a evolução consistente do componente.</p>

<h3>Implementando Variantes com CVA (Class Variance Authority)</h3>

<p>Embora o CVA seja originalmente para Tailwind, seus princípios funcionam perfeitamente em qualquer arquitetura. Vamos criar um sistema de variantes usando uma abordagem funcional pura:</p>

<pre><code class="language-javascript">// variants.js

export const createVariants = (baseStyles, variantMap) =&gt; {

return (variantConfig = {}) =&gt; {

let styles = { ...baseStyles };

Object.entries(variantConfig).forEach(([key, value]) =&gt; {

if (variantMap[key] &amp;&amp; variantMap[key][value]) {

styles = { ...styles, ...variantMap[key][value] };

}

});

return styles;

};

};</code></pre>

<p>Agora criamos um componente Button com múltiplas variantes:</p>

<pre><code class="language-javascript">// ButtonWithVariants.jsx

import styled from &#039;styled-components&#039;;

import { useTokens } from &#039;./ThemeProvider&#039;;

import { createVariants } from &#039;./variants&#039;;

const ButtonBase = styled.button`

font-family: ${props =&gt; props.tokens.typography.fontFamily.sans};

border: none;

cursor: pointer;

transition: all ${props =&gt; props.tokens.transitions.base};

font-weight: ${props =&gt; props.tokens.typography.fontWeight.semibold};

&amp;:disabled {

opacity: 0.6;

cursor: not-allowed;

}

`;

const Button = ({

children,

variant = &#039;primary&#039;,

size = &#039;md&#039;,

loading = false,

...props

}) =&gt; {

const tokens = useTokens();

const sizeVariants = {

sm: {

padding: ${tokens.spacing.sm} ${tokens.spacing.md},

fontSize: tokens.typography.fontSize.sm,

borderRadius: tokens.borderRadius.sm,

},

md: {

padding: ${tokens.spacing.md} ${tokens.spacing.lg},

fontSize: tokens.typography.fontSize.base,

borderRadius: tokens.borderRadius.md,

},

lg: {

padding: ${tokens.spacing.lg} ${tokens.spacing.xl},

fontSize: tokens.typography.fontSize.lg,

borderRadius: tokens.borderRadius.lg,

},

};

const colorVariants = {

primary: {

backgroundColor: tokens.colors.primary[600],

color: &#039;#FFFFFF&#039;,

&#039;&amp;:hover&#039;: {

backgroundColor: tokens.colors.primary[700],

},

},

secondary: {

backgroundColor: tokens.colors.neutral[200],

color: tokens.colors.neutral[900],

&#039;&amp;:hover&#039;: {

backgroundColor: tokens.colors.neutral[300],

},

},

danger: {

backgroundColor: tokens.colors.semantic.error,

color: &#039;#FFFFFF&#039;,

&#039;&amp;:hover&#039;: {

backgroundColor: &#039;#DC2626&#039;,

},

},

};

const getStyles = createVariants(

{ ...sizeVariants[size], ...colorVariants[variant] },

{}

);

const styles = getStyles();

const StyledButton = styled(ButtonBase)`

padding: ${styles.padding};

font-size: ${styles.fontSize};

border-radius: ${styles.borderRadius};

background-color: ${styles.backgroundColor};

color: ${styles.color};

&amp;:hover:not(:disabled) {

background-color: ${styles[&#039;&amp;:hover&#039;].backgroundColor};

}

`;

return (

&lt;StyledButton

tokens={tokens}

disabled={loading || props.disabled}

{...props}

&gt;

{loading ? &#039;⏳ Carregando...&#039; : children}

&lt;/StyledButton&gt;

);

};

export default Button;</code></pre>

<h3>Composição de Componentes Complexos</h3>

<p>Componentes complexos são construídos combinando componentes simples. Vamos criar um Card que usa Button:</p>

<pre><code class="language-javascript">// Card.jsx

import styled from &#039;styled-components&#039;;

import { useTokens } from &#039;./ThemeProvider&#039;;

const CardContainer = styled.div`

background-color: #FFFFFF;

border-radius: ${props =&gt; props.tokens.borderRadius.lg};

box-shadow: ${props =&gt; props.tokens.shadows.md};

overflow: hidden;

`;

const CardHeader = styled.div`

padding: ${props =&gt; props.tokens.spacing.lg};

border-bottom: 1px solid ${props =&gt; props.tokens.colors.neutral[200]};

`;

const CardBody = styled.div`

padding: ${props =&gt; props.tokens.spacing.lg};

`;

const CardFooter = styled.div`

padding: ${props =&gt; props.tokens.spacing.lg};

background-color: ${props =&gt; props.tokens.colors.neutral[50]};

border-top: 1px solid ${props =&gt; props.tokens.colors.neutral[200]};

display: flex;

gap: ${props =&gt; props.tokens.spacing.md};

justify-content: flex-end;

`;

const Card = ({ title, children, onSubmit, onCancel }) =&gt; {

const tokens = useTokens();

return (

&lt;CardContainer tokens={tokens}&gt;

{title &amp;&amp; (

&lt;CardHeader tokens={tokens}&gt;

&lt;h2 style={{ margin: 0, fontSize: tokens.typography.fontSize.xl }}&gt;

{title}

&lt;/h2&gt;

&lt;/CardHeader&gt;

)}

&lt;CardBody tokens={tokens}&gt;

{children}

&lt;/CardBody&gt;

{(onSubmit || onCancel) &amp;&amp; (

&lt;CardFooter tokens={tokens}&gt;

{onCancel &amp;&amp; (

&lt;Button variant=&quot;secondary&quot; onClick={onCancel}&gt;

Cancelar

&lt;/Button&gt;

)}

{onSubmit &amp;&amp; (

&lt;Button variant=&quot;primary&quot; onClick={onSubmit}&gt;

Confirmar

&lt;/Button&gt;

)}

&lt;/CardFooter&gt;

)}

&lt;/CardContainer&gt;

);

};

export default Card;</code></pre>

<h2>Acessibilidade: Tornando o Design System Inclusivo</h2>

<p>Acessibilidade não é um &quot;extra&quot; — é um requisito fundamental. Um design system inacessível exclui milhões de usuários e pode violar leis de conformidade como WCAG 2.1. Todos os componentes devem ser testados com leitores de tela, navegação por teclado e ferramentas de contraste de cor desde o início do desenvolvimento.</p>

<h3>Princípios WCAG 2.1 em Componentes React</h3>

<p>Os quatro princípios WCAG são: <strong>Perceptível</strong>, <strong>Operável</strong>, <strong>Compreensível</strong> e <strong>Robusto</strong>. Vamos aplicá-los a um componente Input:</p>

<pre><code class="language-javascript">// Input.jsx

import styled from &#039;styled-components&#039;;

import { useTokens } from &#039;./ThemeProvider&#039;;

import { useState } from &#039;react&#039;;

const InputWrapper = styled.div`

display: flex;

flex-direction: column;

gap: ${props =&gt; props.tokens.spacing.sm};

`;

const Label = styled.label`

font-size: ${props =&gt; props.tokens.typography.fontSize.sm};

font-weight: ${props =&gt; props.tokens.typography.fontWeight.semibold};

color: ${props =&gt; props.tokens.colors.neutral[700]};

`;

const InputField = styled.input`

padding: ${props =&gt; props.tokens.spacing.sm} ${props =&gt; props.tokens.spacing.md};

font-size: ${props =&gt; props.tokens.typography.fontSize.base};

border: 2px solid ${props =&gt; props.tokens.colors.neutral[300]};

border-radius: ${props =&gt; props.tokens.borderRadius.md};

font-family: ${props =&gt; props.tokens.typography.fontFamily.sans};

transition: all ${props =&gt; props.tokens.transitions.base};

&amp;:focus {

outline: none;

border-color: ${props =&gt; props.tokens.colors.primary[600]};

box-shadow: 0 0 0 3px ${props =&gt; props.tokens.colors.primary[50]};

}

&amp;:disabled {

background-color: ${props =&gt; props.tokens.colors.neutral[100]};

cursor: not-allowed;

opacity: 0.6;

}

&amp;[aria-invalid=&quot;true&quot;] {

border-color: ${props =&gt; props.tokens.colors.semantic.error};

}

`;

const ErrorMessage = styled.span`

font-size: ${props =&gt; props.tokens.typography.fontSize.sm};

color: ${props =&gt; props.tokens.colors.semantic.error};

display: flex;

align-items: center;

gap: ${props =&gt; props.tokens.spacing.xs};

`;

const HelperText = styled.span`

font-size: ${props =&gt; props.tokens.typography.fontSize.xs};

color: ${props =&gt; props.tokens.colors.neutral[500]};

`;

const Input = ({

id,

label,

type = &#039;text&#039;,

placeholder,

value,

onChange,

onBlur,

error,

helperText,

required = false,

disabled = false,

...props

}) =&gt; {

const tokens = useTokens();

const [isFocused, setIsFocused] = useState(false);

const inputId = id || input-${Math.random().toString(36).slice(2, 9)};

const errorId = ${inputId}-error;

const helperId = ${inputId}-helper;

const handleBlur = (e) =&gt; {

setIsFocused(false);

onBlur?.(e);

};

const handleFocus = () =&gt; {

setIsFocused(true);

};

const ariaDescribedBy = [

error ? errorId : null,

helperText ? helperId : null,

].filter(Boolean).join(&#039; &#039;);

return (

&lt;InputWrapper tokens={tokens}&gt;

{label &amp;&amp; (

&lt;Label htmlFor={inputId}&gt;

{label}

{required &amp;&amp; (

&lt;span

aria-label=&quot;obrigatório&quot;

style={{ color: tokens.colors.semantic.error }}

&gt;

*

&lt;/span&gt;

)}

&lt;/Label&gt;

)}

&lt;InputField

id={inputId}

type={type}

placeholder={placeholder}

value={value}

onChange={onChange}

onBlur={handleBlur}

onFocus={handleFocus}

disabled={disabled}

required={required}

aria-invalid={!!error}

aria-describedby={ariaDescribedBy || undefined}

tokens={tokens}

{...props}

/&gt;

{error &amp;&amp; (

&lt;ErrorMessage id={errorId} role=&quot;alert&quot; tokens={tokens}&gt;

⚠️ {error}

&lt;/ErrorMessage&gt;

)}

{helperText &amp;&amp; !error &amp;&amp; (

&lt;HelperText id={helperId} tokens={tokens}&gt;

{helperText}

&lt;/HelperText&gt;

)}

&lt;/InputWrapper&gt;

);

};

export default Input;</code></pre>

<h3>Testando Acessibilidade</h3>

<p>Para garantir que seu design system é realmente acessível, teste com ferramentas automatizadas e testes manuais:</p>

<pre><code class="language-javascript">// Button.test.jsx - usando Jest e Testing Library

import { render, screen } from &#039;@testing-library/react&#039;;

import userEvent from &#039;@testing-library/user-event&#039;;

import Button from &#039;./Button&#039;;

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

describe(&#039;Button Accessibility&#039;, () =&gt; {

const renderButton = (props) =&gt;

render(

&lt;ThemeProvider&gt;

&lt;Button {...props}&gt;Click me&lt;/Button&gt;

&lt;/ThemeProvider&gt;

);

test(&#039;deve ser focável via teclado&#039;, () =&gt; {

renderButton();

const button = screen.getByRole(&#039;button&#039;);

button.focus();

expect(button).toHaveFocus();

});

test(&#039;deve ser clicável com Enter e Space&#039;, async () =&gt; {

const user = userEvent.setup();

const handleClick = jest.fn();

renderButton({ onClick: handleClick });

const button = screen.getByRole(&#039;button&#039;);

button.focus();

await user.keyboard(&#039;{Enter}&#039;);

expect(handleClick).toHaveBeenCalled();

});

test(&#039;deve ter contraste de cor suficiente&#039;, () =&gt; {

renderButton({ variant: &#039;primary&#039; });

const button = screen.getByRole(&#039;button&#039;);

const styles = window.getComputedStyle(button);

// Verificar que há uma cor de fundo e texto definidas

expect(styles.backgroundColor).toBeTruthy();

expect(styles.color).toBeTruthy();

});

test(&#039;deve desabilitar corretamente com aria-disabled&#039;, () =&gt; {

renderButton({ disabled: true });

const button = screen.getByRole(&#039;button&#039;, { hidden: true });

expect(button).toBeDisabled();

});

test(&#039;deve ter texto descritivo adequado&#039;, () =&gt; {

renderButton({ children: &#039;Enviar Formulário&#039; });

expect(screen.getByRole(&#039;button&#039;)).toHaveAccessibleName(

&#039;Enviar Formulário&#039;

);

});

});</code></pre>

<h2>Integrando Tudo: Um Design System Completo em Ação</h2>

<p>Agora vamos demonstrar como integrar todos esses conceitos em uma aplicação real:</p>

<pre><code class="language-javascript"></code></pre>

<h2>Conclusão</h2>

<p>Você aprendeu que um <strong>Design System sólido é construído em três pilares</strong>: tokens bem estruturados que centralizam decisões visuais, variantes de componentes que oferecem flexibilidade sem sacrificar consistência, e acessibilidade integrada desde o início, não como &quot;bônus&quot;. O React oferece as abstrações perfeitas para implementar esses conceitos através de Context API, componentes compostos e props bem definidas.</p>

<p>Na prática, isso significa que você consegue manter uma aplicação complexa com dezenas ou centenas de componentes de forma escalável, permitindo mudanças globais de branding em minutos e garantindo que todos os usuários, independentemente de suas capacidades, possam usar sua aplicação. O investimento inicial em construir um bom design system economiza tempo exponencialmente em desenvolvimento futuro.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.w3.org/WAI/WCAG21/quickref/" target="_blank" rel="noopener noreferrer">WCAG 2.1 Guidelines - W3C</a></li>

<li><a href="https://react.dev/reference/react/useContext" target="_blank" rel="noopener noreferrer">React Documentation - Context API</a></li>

<li><a href="https://storybook.js.org/" target="_blank" rel="noopener noreferrer">Storybook - Component Driven Development</a></li>

<li><a href="https://www.figma.com/community/file/1184664993926163221" target="_blank" rel="noopener noreferrer">Design Tokens - Figma Guide</a></li>

<li><a href="https://styled-components.com/docs" target="_blank" rel="noopener noreferrer">Styled Components Documentation</a></li>

</ul>

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

Comentários

Mais em React & Frontend

Como Usar Redux Toolkit Moderno: Slices, RTK Query e Thunks Tipados em Produção
Como Usar Redux Toolkit Moderno: Slices, RTK Query e Thunks Tipados em Produção

Entendendo Redux Toolkit: Fundamentos e Filosofia Redux é uma biblioteca de g...

Guia Completo de React.memo em Profundidade: Quando Usar, Quando Evitar
Guia Completo de React.memo em Profundidade: Quando Usar, Quando Evitar

O Que é React.memo e Por Que Existe React.memo é um HOC (Higher Order Compone...

Como Usar Virtualização de Listas em React: react-window e react-virtual em Produção
Como Usar Virtualização de Listas em React: react-window e react-virtual em Produção

O Problema da Renderização em Listas Grandes Quando trabalha com listas conte...