Garantir a legibilidade do texto com cores de plano de fundo dinâmicas

Recursos da Web: contrast-color()

Ao criar componentes de interface reutilizáveis (como selos ou botões) ou oferecer suporte a temas dinâmicos, garantir que o texto seja legível em uma cor de plano de fundo imprevisível pode ser um desafio. A função CSS contrast-color() é resolvida como black ou white, maximizando a proporção de contraste em relação à cor de fundo especificada.

Determinar a cor ideal do texto

Quando um elemento tem uma cor de plano de fundo variável ou injetada dinamicamente, você pode confiar no navegador para calcular a cor de texto mais segura.

OBRIGATÓRIO: defina a propriedade color usando a função contrast-color() e transmita a cor de fundo (ou a propriedade personalizada dela) como argumento.

.badge {
  /* Define the background color as a custom property */
  --badge-bg: #e02424;
  background-color: var(--badge-bg);
  /* MANDATORY: Dynamic contrasting color */
  color: contrast-color(var(--badge-bg));
}

NÃO use contrast-color() para background-color. Ela foi projetada especificamente para que as cores de primeiro plano (como texto ou bordas) contrastem com uma determinada cor de plano de fundo.

Integrar com temas

Como contrast-color() aceita qualquer valor <color> válido, incluindo propriedades personalizadas de CSS, ele é especialmente útil em sistemas de design. Ao centralizar os temas de plano de fundo como variáveis, você garante que o conteúdo em primeiro plano atualize automaticamente o contraste sempre que o tema mudar, mesmo para cores especificadas pelo usuário.

RECOMENDAÇÃO: faça referência a variáveis de segundo plano compartilhadas dentro de contrast-color() para garantir a sincronização em todo o aplicativo.

.theme-card {
  background-color: var(--theme-surface-color);
  color: contrast-color(var(--theme-surface-color));
}

.theme-container[data-theme="light"] {
  --theme-surface-color: #f4f4f4;
}
.theme-container[data-theme="dark"] {
  --theme-surface-color: #1a1a1a;
}
.theme-container[data-theme="custom"] {
  --theme-surface-color: var(--user-custom-theme);
}

NÃO FAÇA: codificar uma cor de texto separada para cada estado de interação quando o plano de fundo for dinâmico. Isso reintroduz a incompatibilidade que o contrast-color() existe para evitar.

Estratégias de fallback

Para oferecer suporte a navegadores sem contrast-color(), é necessário fornecer um substituto. Se a cor do plano de fundo for conhecida e fixa, use uma cor contrastante codificada. Para planos de fundo dinâmicos em que a cor é desconhecida, escolha uma estratégia na tabela abaixo com base nos requisitos da interface e nas metas de suporte do navegador.

Estratégia Ideal para… Considerações
Sintaxe de cor relativa Cálculo de contraste automatizado e de alta qualidade somente com CSS. Substituto somente CSS de alta qualidade.
Sombra do texto Melhora rápida da legibilidade em qualquer plano de fundo. Pode parecer "sujo" ou "brilhante" e não se adequar a todos os designs.
Traço do texto Preservação da espessura da fonte e garantia de contraste de borda. Use paint-order para evitar o afinamento de formas de letras, se disponível.
Sobreposição translúcida Garantir uma área de contraste mínima atrás do texto. Muda a aparência da cor do plano de fundo sob o texto, requer um elemento do texto separado.
Filtros SVG Contraste reativo que se atualiza conforme o plano de fundo muda. Requer um elemento do texto separado; implementação de hack.

Para navegadores compatíveis, o RCS oferece o substituto automatizado mais robusto para cores dinâmicas.

@supports (color: oklch(from red l c h)) {
  .badge {
    /* Highest threshold that passes WCAG. Higher values may be more legible. */
    --threshold: 0.623;
    --l: max(0, sign(var(--threshold) - l));
    color: oklch(from var(--badge-bg) var(--l) 0 h);
  }
}

@supports (color: contrast-color(red)) {
  .badge {
    color: contrast-color(var(--badge-bg));
  }
}

Alternativas de CSS

FAÇA: selecione o substituto que melhor se adapta às suas restrições de design se o RCS não for suficiente ou não for compatível.

Opção 1: traçado do texto

.badge--stroke {
  -webkit-text-stroke: 4px black;
}

Opção 2: traço de texto com paint-order para preservar as formas das letras

.badge--stroke {
  -webkit-text-stroke: 4px black;
  paint-order: stroke fill;
}

Opção 3: sobreposição de plano de fundo translúcido

.badge--overlay {
  position: relative;
  color: #fff;
  overflow: hidden;
}
.badge--overlay::before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: rgb(0 0 0 / 0.4);
  z-index: 0;
}
.badge--overlay span {
  position: relative;
  z-index: 1;
}

Opção 4: sombra de texto

.badge--shadow {
  text-shadow: 0 1px 3px rgb(0 0 0 / 0.8);
}

Opção 5: filtros SVG

  1. Carregue esta definição de filtro SVG. Ele precisa ser adicionado diretamente ao documento HTML se você precisar oferecer suporte a versões do Safari afetadas por este bug do WebKit, que impede referências de URL de dados e externas para filtros. Caso contrário, ele precisa ser carregado de um arquivo externo, seja um arquivo SVG ou inline em CSS.

    <!-- Technique from https://miunau.com/posts/dynamic-text-contrast-in-css/ -->
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="0" style="display: none;">
      <defs>
        <filter id="contrast-filter" color-interpolation-filters="sRGB">
          <feColorMatrix type="matrix"
            values="0.2126 0.7152 0.0722 0 0
              0.2126 0.7152 0.0722 0 0
              0.2126 0.7152 0.0722 0 0
              0 0 0 1 0"/>
          <feMorphology operator="dilate" radius="2"/>
          <feComponentTransfer>
            <feFuncR type="linear" slope="-255" intercept="128"/>
            <feFuncG type="linear" slope="-255" intercept="128"/>
            <feFuncB type="linear" slope="-255" intercept="128"/>
          </feComponentTransfer>
          <feComposite operator="in" in2="SourceGraphic"/>
        </filter>
      </defs>
    </svg>
    
  2. Envolva o texto em um <span>.

    <div class="badge--svg">
      <span>Badge content</span>
    </div>
    
  3. Aplique o filtro ao <span>.

.badge--svg span{
  color: var(--badge-bg);
  filter: url(#contrast-filter);
}