# Contraste de cores e acessibilidade: o que a WCAG exige

> A WCAG pede contraste mínimo de 4,5:1 entre o texto e o fundo, e de 3:1 para texto grande e para ícones e bordas de campos, no nível AA, que é o exigido em leis e contratos. Este guia explica o que é a razão de contraste, como ela é calculada a partir da luminância das cores, os erros mais comuns (como arredondar 4,48 para 4,5) e como corrigir uma cor sem mudar a identidade visual.

- Endereço oficial: https://pradev.com.br/guias/contraste-de-cores-wcag-acessibilidade
- Tipo: Guia
- Publicado em: 2026-10-01
- Atualizado em: 2026-10-01
- Autor: Leonardo Gaertner (https://pradev.com.br/sobre)
- Ferramentas: [Verificador de Contraste](https://pradev.com.br/verificador-de-contraste), [Gerador de Paleta a partir de HEX](https://pradev.com.br/gerador-de-paleta-a-partir-de-hex), [Conversor de Cores HEX RGB HSL](https://pradev.com.br/conversor-de-cores)

## Por que o contraste importa

Texto com pouco contraste é difícil de ler para quem tem baixa visão, daltonismo ou catarata, e também para qualquer pessoa no celular ao sol. A WCAG (Web Content Accessibility Guidelines), do W3C, define limites objetivos para isso, e é a referência usada pela Lei Brasileira de Inclusão, pelo eMAG do governo federal e por auditorias de acessibilidade.

O contraste também conta para a nota de acessibilidade do Lighthouse, que aponta cada texto abaixo do limite. Para conferir uma combinação na hora, use o [verificador de contraste](https://pradev.com.br/verificador-de-contraste).

## Os limites da WCAG 2.2

- Texto normal: 4,5:1 no nível AA e 7:1 no AAA.
- Texto grande (a partir de 24px, ou 18,66px em negrito): 3:1 no AA e 4,5:1 no AAA.
- Ícones, bordas de campos, foco e outros elementos gráficos necessários para entender a interface: 3:1.
- Ficam de fora logotipos, texto decorativo e elementos desabilitados.

O nível exigido na prática é o AA. O AAA é recomendado para textos longos e públicos específicos, mas nem sempre é possível em toda a interface.

## Como a razão de contraste é calculada

Primeiro, cada canal da cor (vermelho, verde e azul, de 0 a 255) é convertido para luz linear, desfazendo a curva de gama do sRGB. Depois, a luminância relativa soma os canais com pesos diferentes, porque o olho é mais sensível ao verde: `L = 0,2126 R + 0,7152 G + 0,0722 B`. A razão é `(L1 + 0,05) ÷ (L2 + 0,05)`, com L1 da cor mais clara. Preto sobre branco dá 21:1, o máximo.

```javascript
// Razão de contraste da WCAG 2 entre duas cores em HEX ("#777777" e "#ffffff")
function luminancia(hex) {
  const n = parseInt(hex.replace('#', ''), 16);
  const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => {
    const c = v / 255;
    return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; // canal em luz linear
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function contraste(cor1, cor2) {
  const [clara, escura] = [luminancia(cor1), luminancia(cor2)].sort((a, b) => b - a);
  return (clara + 0.05) / (escura + 0.05);
}

const r = contraste('#777777', '#ffffff');
console.log(r.toFixed(2), r >= 4.5 ? 'passa no AA' : 'não passa no AA'); // 4.48 não passa no AA (a razão exata é 4,478...)
```

_JavaScript. Razão de contraste entre duas cores em HEX._

```python
def luminancia(cor: str) -> float:
    """Luminância relativa da WCAG 2 de uma cor em HEX de 6 dígitos."""
    h = cor.lstrip("#")
    canais = []
    for i in (0, 2, 4):
        c = int(h[i : i + 2], 16) / 255
        canais.append(c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4)
    r, g, b = canais
    return 0.2126 * r + 0.7152 * g + 0.0722 * b


def contraste(cor1: str, cor2: str) -> float:
    clara, escura = sorted((luminancia(cor1), luminancia(cor2)), reverse=True)
    return (clara + 0.05) / (escura + 0.05)


r = contraste("#777777", "#ffffff")
print(f"{r:.2f}", "passa no AA" if r >= 4.5 else "não passa no AA")  # 4.48 não passa no AA (a razão exata é 4,478...)
```

_Python_

```php
<?php
// Luminância relativa da WCAG 2 de uma cor em HEX de 6 dígitos
function luminancia(string $cor): float
{
    [$r, $g, $b] = array_map(function (string $par): float {
        $c = hexdec($par) / 255;
        return $c <= 0.04045 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4;
    }, str_split(ltrim($cor, '#'), 2));
    return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
}

function contraste(string $cor1, string $cor2): float
{
    $l = [luminancia($cor1), luminancia($cor2)];
    return (max($l) + 0.05) / (min($l) + 0.05);
}

$r = contraste('#777777', '#ffffff');
printf("%.2f %s\n", $r, $r >= 4.5 ? 'passa no AA' : 'não passa no AA'); // 4.48 não passa no AA (a razão exata é 4,478...)
```

_PHP_

## Erros comuns

- Arredondar para cima: #777777 sobre branco dá 4,478:1. Exibido com duas casas, vira 4,48, mas não chega a 4,5 e não passa no AA. Compare o valor exato com o limite.
- Esquecer a transparência: um texto preto com 50% de opacidade sobre branco equivale a um cinza médio. Calcule com a cor que aparece de fato, depois de sobrepor ao fundo.
- Testar só o tema claro: no modo escuro, as cores mudam e o contraste precisa ser conferido de novo.
- Confiar no placeholder: o texto de exemplo dentro de um campo também é texto, e costuma ficar claro demais.

## Como corrigir uma cor sem mudar a identidade

Para manter a cor da marca e ainda passar, mude só a luz e preserve o tom. No espaço OKLCH, isso é direto: escurecer o azul #3b82f6 sobre branco até passar no AA dá um azul mais fechado, ainda reconhecível. O verificador de contraste faz essa busca e sugere a cor mais próxima que passa no AA e no AAA.

Outra saída é planejar a paleta com uma escala de tons, como a do [gerador de paleta](https://pradev.com.br/gerador-de-paleta-a-partir-de-hex): os tons 50 a 100 servem de fundo, e os tons de 700 em diante, de texto. Para ver a cor em outros formatos, como HSL e OKLCH, use o [conversor de cores](https://pradev.com.br/conversor-de-cores).

## Perguntas frequentes

### Qual o contraste mínimo para texto?

No nível AA da WCAG, 4,5:1 para texto normal e 3:1 para texto grande, a partir de 24px ou de 18,66px em negrito. No AAA, 7:1 e 4,5:1.

### Cinza #777 sobre branco é acessível?

Para texto normal, não: a razão é 4,48:1, abaixo de 4,5. Passa para texto grande e para ícones. O cinza mais claro que passa no AA sobre branco é #767676.

### O contraste do texto sobre imagem também conta?

Sim. Como a imagem varia, confira o contraste no pior ponto ou ponha uma camada escura ou clara por trás do texto, para garantir o mínimo em toda a área.

### A WCAG 3 muda esses números?

A WCAG 3 está em elaboração e pode adotar outro método, o APCA. Enquanto ela não é norma, leis e auditorias seguem a WCAG 2, com os limites deste guia.

## Fontes

- [W3C: WCAG 2.2, critério 1.4.3 (contraste mínimo)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html)
- [W3C: WCAG 2.2, critério 1.4.11 (contraste de elementos gráficos)](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html)
- [W3C: definição de luminância relativa](https://www.w3.org/TR/WCAG22/#dfn-relative-luminance)

## Guias relacionados

- [OKLCH no CSS: cores e paletas que fazem sentido](https://pradev.com.br/guias/oklch-no-css-cores-e-paletas): O que é OKLCH, por que ele gera paletas mais equilibradas que HSL e como usar oklch() no CSS, com a conversão de HEX em JavaScript e Python.
