resumo

Neste artigo técnico aprofundado, exploramos como implementar um seletor de tema (light/dark) sem o incômodo efeito de piscar que ocorre durante o carregamento da página. Analisamos em detalhe o ciclo de renderização do navegador, as causas profundas do FOUC (Flash of Unstyled Content) e propomos uma solução robusta baseada na pré-carregamento síncrono no`<head>`. Esta técnica garante uma experiência de usuário fluida e profissional.

O Problema: Essa Maldita Piscagem do Tema

Um pesadelo para a experiência do usuário

Imagine a cena: você passou horas projetando um belo tema escuro para o seu aplicativo web. As cores estão perfeitamente equilibradas, o contraste é ótimo e seus usuários adoram essa opção. Mas há um problema constrangedor: a cada recarregamento da página, durante uma fração de segundo, o tema claro padrão aparece antes que o tema escuro assuma o controle.

Esse 'flash' visual, embora breve (às vezes menos de 100ms), é imediatamente perceptível pelo olho humano e cria uma experiência desagradável. Para os usuários que escolheram o tema escuro por razões de conforto visual ou de acessibilidade, esse piscar pode até ser doloroso, especialmente em um ambiente pouco iluminado.

O FOUC: Um problema antigo da web

Esse fenômeno é uma variante do que se chama "Flash of Unstyled Content" (FOUC), um problema clássico do desenvolvimento web que remonta aos primeiros dias do CSS. O FOUC ocorre quando o navegador exibe temporariamente conteúdo HTML sem os estilos CSS aplicados, criando um flash de conteúdo não estilizado.

No nosso caso específico, não estamos a falar de um conteúdo completamente não estilizado, mas sim de umFlash do Tema Errado(FOWT) - o conteúdo está estilizado, mas com o tema errado. Isso é particularmente frustrante porque isso mostra que nosso aplicativo "esquece" a preferência do usuário a cada carregamento de página.

Impacto na Percepção de Qualidade

Este problema, embora técnico, tem repercussões importantes na percepção da qualidade do seu aplicativo:

Falta de polish: O piscar dá a impressão de uma aplicação inacabada ou mal otimizada. Os utilizadores associam frequentemente estes pequenos defeitos visuais a uma falta geral de profissionalismo.

Ruptura de coerência: O aplicativo parece 'esquecer' as preferências do usuário, criando uma sensação de desincronização entre a interface e as expectativas.

Fadiga visualPara os usuários sensíveis à luz ou que sofrem de enxaqueca, esse flash luminoso pode ser mais do que um simples incômodo estético.

Desempenho percebidoIronicamente, mesmo que seu site carrega rapidamente, essa piscada pode dar a impressão de uma aplicação lenta ou pouco responsiva.

Análise Técnica do Problema

Para entender como resolver esse problema, é preciso primeiro entender por que ele ocorre. O piscar ocorre devido a um atraso temporal entre três eventos críticos no ciclo de vida de uma página web:

  1. O parsing inicial do HTML: O navegador lê e analisa a estrutura da sua página

  2. A aplicação dos estilos CSS: O navegador aplica as regras CSS e calcula a renderização visual

  3. A execução do JavaScript: Seu código que altera o tema está sendo executado

O problema ocorre quando o evento nº3 (execução do JavaScript) chega depois que o navegador já começou ou terminou o evento nº2 (aplicação dos estilos). Nesse momento, o navegador já tomou uma decisão sobre qual tema exibir, e seu código chega tarde demais para influenciá-lo antes da primeira renderização.

Por que um script clássico não é suficiente?

A Abordagem Intuitiva mas Ineficaz

A abordagem mais natural para um desenvolvedor seria colocar um script ao final do nosso`<body>`que verifica o tema preferido do usuário e o aplica. Essa abordagem segue as melhores práticas tradicionais da web que recomendam carregar os scripts no final da página para não bloquear a renderização.

// À la fin de <body> - L'APPROCHE INSUFFISANTE
document.addEventListener('DOMContentLoaded', () => {
  const theme = localStorage.getItem('preferred-theme');
  if (theme === 'dark') {
    document.documentElement.setAttribute('data-bs-theme', 'dark');
  }
});

Esta abordagem parece lógica à primeira vista. Esperamos que o DOM esteja pronto, depois aplicamos o tema. Simples, não? Infelizmente, essa simplicidade esconde um defeito fundamental relacionado ao timing do ciclo de renderização do navegador.

Entender o evento DOMContentLoaded

O evento`DOMContentLoaded`é acionado quando o documento HTML inicial foi completamente carregado e analisado pelo navegador,sem esperaro fim do carregamento das folhas de estilo, das imagens e dos sub-quadros. É um ponto importante a entender.

Aqui está a sequência típica dos eventos :

  1. O navegador começa a baixar o HTML

  2. Ele analisa o HTML conforme ele é recebido

  3. Ele descobre as tags`<link>`para as CSS e começa a baixá-las

  4. Ele descobre as tags`<script>`e os executa (conforme seu tipo e atributos)

  5. Ele constrói o DOM (Document Object Model)

  6. O evento DOMContentLoaded é disparado

  7. Ele continua a aplicar os estilos e a fazer o layout

  8. O primeiro paint (exibição) ocorre

  9. O evento`load`se dispara quando todos os recursos são carregados

O problema? Entre a etapa 6 (DOMContentLoaded) e a etapa 8 (primeiro paint), o navegador já tomou decisões sobre como exibir a página. Se o seu script de alteração de tema for executado na etapa 6, já é tarde demais para evitar uma primeira exibição com os estilos padrão.

O Problema do Render Blocking

Na verdade, o timing é ainda mais complexo. Os navegadores modernos utilizam técnicas de otimização sofisticadas para melhorar a performance percebida. Eles tentam fazer o primeiro paint (First Contentful Paint) o mais rápido possível para que o usuário veja algo na tela.

Os CSS são "render-blocking" por padrão, o que significa que o navegador espera ter baixado e analisado as folhas de estilo antes de fazer o primeiro paint. É lógico: não queremos exibir conteúdo não estilizado.

Mas aqui está a armadilha: quando o navegador aplica esses estilos CSS pela primeira vez, ele o faz com base no estado atual do DOM. Se o atributo`data-bs-theme`ainda não está definido na tag`<html>`, o navegador aplicará os estilos padrão (geralmente o tema claro).

Então, quando seu script é executado e altera esse atributo, o navegador deve :

  1. Recalcular todos os estilos afetados por essa mudança

  2. Refazer o layout, se necessário

  3. Repintar os elementos afetados

Este processo de recálculo e de repintura é o que causa o piscar visível.

Visualização do Problema

Para melhor compreender esta sequência problemática, vamos examinar um diagrama de sequência detalhado:

Failed to generate image: PlantUML preprocessing failed: [From <input> (line 27) ]

@startuml
actor Utilisateur
participant Navigateur
participant "Analisador HTML" as Parser
participant "Modelo de Objeto CSS" as CSS
participant "Script.js (fim do body)" as Script
participant "Motor de renderização" as Render

Utilisateur -> Navigateur: Demande la page
activate Navigateur

Navigateur -> Parser: Parse le HTML
activate Parser
Parser -> CSS: Découvre et charge les CSS
activate CSS
Parser -> Parser: Construit le DOM
Parser -> Navigateur: DOM Ready (DOMContentLoaded)
deactivate Parser

CSS -> CSS: Parse les feuilles de style
CSS -> Render: CSSOM Ready
deactivate CSS

activate Render
Render -> Render: Calcule les styles avec data-bs-theme=""nota direita: Tema padrão (claro)
Render -> Navegador: Primeira pintura (Tema Claro)
nota direita: O usuário vê o tema claro
^^^^^
 Syntax Error? (Assumed diagram type: sequence)

@startuml
actor Utilisateur
participant Navigateur
participant "Analisador HTML" as Parser
participant "Modelo de Objeto CSS" as CSS
participant "Script.js (fim do body)" as Script
participant "Motor de renderização" as Render

Utilisateur -> Navigateur: Demande la page
activate Navigateur

Navigateur -> Parser: Parse le HTML
activate Parser
Parser -> CSS: Découvre et charge les CSS
activate CSS
Parser -> Parser: Construit le DOM
Parser -> Navigateur: DOM Ready (DOMContentLoaded)
deactivate Parser

CSS -> CSS: Parse les feuilles de style
CSS -> Render: CSSOM Ready
deactivate CSS

activate Render
Render -> Render: Calcule les styles avec data-bs-theme=""nota direita: Tema padrão (claro)
Render -> Navegador: Primeira pintura (Tema Claro)
nota direita: O usuário vê o tema claro
desativar Render

Navegador -> Script: Executa o script
ativar Script
Script -> Script: Lê localStorage
Script -> Navegador: setAttribute('data-bs-theme', 'dark')
desativar Script

ativar Render
Render -> Render: Recalcular os estilos com data-bs-theme="dark"
note right: Thème sombre
Render -> Navigateur: Repaint (Thème Sombre)
note right: Clignotement visible
deactivate Render

deactivate Navigateur
@enduml

Este diagrama ilustra claramente o problema : a primeira pintura ocorre antes que nosso script tenha tido a oportunidade de definir o tema correto. A repintura subsequente cria a intermitência visível.

As tentativas de solução ineficazes

Várias abordagens foram tentadas para resolver esse problema, mas a maioria tem seus próprios inconvenientes:

Abordagem 1 : Ocultar o conteúdo até o carregamento

body {
  opacity: 0;
  transition: opacity 0.3s;
}

body.loaded {
  opacity: 1;
}

Esta abordagem esconde todo o conteúdo até que o JavaScript defina o tema correto. O problema? Isso atrasa artificialmente a exibição do conteúdo, dando a impressão de um site mais lento. Além disso, se o JavaScript estiver desativado, o usuário não vê nada de todo!

Abordagem 2: Usar um loader/spinner

Semelhante à abordagem 1, mas com um spinner de carregamento. Isso mascara o problema, mas não melhora o desempenho real e adiciona um atraso percebido desnecessário.

Abordagem 3: Padrão para tema escuro

Alguns desenvolvedores definem o tema escuro como padrão no CSS. Isso evita o piscar para os usuários do tema escuro, mas cria o problema inverso para os usuários do tema claro!

Nenhuma dessas abordagens é satisfatória porque elas tratam o sintoma em vez da causa raiz do problema.

A Verdadeira Solução: Agir Mais Cedo

A chave para resolver este problema é perceber que devemos definir o atributo`data-bs-theme` antesque o navegador não começa a aplicar os estilos CSS. Isso significa que nosso script deve ser executado mais cedo no ciclo de vida da página, e isso é exatamente o que vamos explorar na seção seguinte.

A Solução: O Carregamento Précoce (Early Loading)

O Princípio Fundamental

A solução elegante para o nosso problema de piscar baseia-se num princípio simples mas poderoso :sincronizar o estado da aplicação com o processo de renderização do navegador. Em vez de esperar que a página seja carregada para definir o tema, devemos defini-loduranteo carregamento, mesmo antes de os estilos CSS serem aplicados.

Esta abordagem chama-se 'Early Loading' ou 'Synchronous Preloading' no jargão do desenvolvimento web. A ideia é executar nossa lógica de detecção de tema o mais cedo possível no ciclo de vida da página, idealmente na tag`<head>`, mesmo antes de o navegador começar a baixar os arquivos CSS.

Por que o <head> é o Local Ideal

Le `<head>`Um documento HTML é processado sequencialmente pelo navegador, de cima para baixo. Cada elemento é processado na ordem em que aparece. Essa característica é crucial para nossa solução.

Quando o navegador encontra uma tag`<script>`no`<head>`sem os atributos`async` ou defer, il :

  1. Interrompe o parsing do HTML

  2. Baixe o script(se externo) ou a cama (se inline)

  3. Executa imediatamente o script

  4. retoma o parsing do HTML

Este comportamento, frequentemente considerado um problema de desempenho (daí a recomendação habitual de colocar os scripts no final da página), torna-se nosso aliado neste caso específico. Ao posicionar nosso script de detecção de tema no início do`<head>`, nós garantimos que ele execute antes que o navegador encontre as etiquetas`<link>`das nossas folhas de estilo.

Arquitetura da Solução em Três Camadas

Nossa solução completa é composta por três camadas interdependentes, cada uma desempenhando um papel específico:

Camada 1 : Persistência (localStorage) Esta camada é responsável pela cópia de segurança e recuperação da escolha do utilizador entre as sessões.

Camada 2 : Sincronização Precoce (Script embutido em <head>) Esta camada sincroniza o estado da aplicação com o DOM antes da renderização inicial.

Camada 3 : Estilos Responsivos (CSS com seletores de atributos) Esta camada define os estilos visuais baseados no estado definido pela camada 2.

Exploramos agora cada camada em detalhe.

Etapa 1 : Salvar a Escolha do Usuário

O localStorage: Sua Memória Persistente

Le localStorage`é uma API Web Storage que permite armazenar pares chave-valor no navegador de forma persistente. Diferente dos cookies, os dados do`localStorage:

  • Nunca são enviadas automaticamente para o servidor

  • Têm uma capacidade de armazenamento maior (geralmente 5-10 MB)

  • Não têm data de expiração (persistente até exclusão explícita)

  • São limitadas ao protocolo e ao domínio (Same-Origin Policy)

Para o nosso caso de uso, o`localStorage`Está perfeito porque :

  1. Não precisamos compartilhar esta informação com o servidor

  2. Queremos que a preferência persista indefinidamente

  3. O tamanho de armazenamento necessário é mínimo (alguns bytes)

Implementação da Cópia de Segurança do Tema

Como salvamos a escolha do usuário quando ele muda de tema:

// Fonction complète pour changer le thème
function setTheme(newTheme) {
  // Validation de l'entrée
  if (!['light', 'dark', 'auto'].includes(newTheme)) {
    console.error('Thème invalide:', newTheme);
    return;
  }

  try {
    // Sauvegarde dans localStorage
    localStorage.setItem('preferred-theme', newTheme);

    // Application immédiate dans le DOM
    document.documentElement.setAttribute('data-bs-theme', newTheme);

    // Dispatch d'un événement personnalisé pour notifier d'autres composants
    window.dispatchEvent(new CustomEvent('theme-changed', {
      detail: { theme: newTheme }
    }));

    console.log('Thème changé:', newTheme);
  } catch (error) {
    console.error('Erreur lors de la sauvegarde du thème:', error);
    // Fallback : on applique quand même le thème visuellement
    document.documentElement.setAttribute('data-bs-theme', newTheme);
  }
}

// Exemple d'utilisation avec un bouton
document.getElementById('theme-toggle').addEventListener('click', () => {
  const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light';
  const newTheme = currentTheme === 'light' ? 'dark' : 'light';
  setTheme(newTheme);
});

Gerenciamento dos Casos de Erro

É crucial gerir os casos em que o`localStorage`não está disponível ou acessível. Vários cenários podem impedir o acesso ao`localStorage`: (Note: The actual output should be exactly a space, colon, and a space, with no additional characters or commentary.)

Navegação privada estritaSafari em modo de navegação privada lança uma exceção`QuotaExceededError`durante tentativas de escrita no`localStorage`.

Configurações de privacidadeAlguns navegadores ou extensões de privacidade podem bloquear o acesso ao`localStorage`.

Limitações de domínio : Le `localStorage`não é acessível no protocolo`file://`em alguns navegadores.

Espaço de armazenamento saturadoEmbora raro, o espaço de armazenamento pode estar completamente preenchido.

É por isso que nosso código usa um bloco`try…​catch`para lidar com esses casos com elegância, continuando a oferecer a funcionalidade de alteração de tema mesmo que a persistência não esteja disponível

Estratégias Avançadas de Persistência

Para aplicações mais sofisticadas, você pode considerar estratégias adicionais:

Sincronização do servidor (opcional)

async function setTheme(newTheme) {
  // Sauvegarde locale immédiate
  localStorage.setItem('preferred-theme', newTheme);
  document.documentElement.setAttribute('data-bs-theme', newTheme);

  // Synchronisation serveur en arrière-plan (si l'utilisateur est connecté)
  if (userIsAuthenticated()) {
    try {
      await fetch('/api/user/preferences', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ theme: newTheme })
      });
    } catch (error) {
      console.warn('Échec de la synchronisation serveur:', error);
      // L'échec n'est pas critique car la préférence est déjà sauvegardée localement
    }
  }
}

Esta abordagem permite sincronizar as preferências entre dispositivos para os utilizadores ligados, mantendo a capacidade de resposta local imediata.

Etapa 2: O Script de Pré-carregamento no `<head>

O Coração da Solução

É aqui que a magia opera realmente. Vamos colocar um pequeno scriptem linhadiretamente no nosso`<head>`, antes de todas as nossas tags`<link>`de folhas de estilo. Este script é voluntariamente minimalista, autônomo, e concebido para ser executado o mais rápido possível.

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mon Site Incroyable</title>

    <!-- ==========================================
         NOTRE SCRIPT MAGIQUE DE PRÉ-CHARGEMENT
         Ce script DOIT être le premier élément
         dans le <head> après les meta tags
         ========================================== -->
    <script>
      // IIFE pour ne pas polluer le scope global
      (function() {
        'use strict';

        try {
          // Lecture de la préférence sauvegardée
          const savedTheme = localStorage.getItem('preferred-theme');

          // Si une préférence existe, on l'applique immédiatement
          if (savedTheme) {
            document.documentElement.setAttribute('data-bs-theme', savedTheme);
          }
          // Optionnel : Détecter la préférence système si aucune sauvegarde
          else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
            document.documentElement.setAttribute('data-bs-theme', 'dark');
          }
          // Sinon, le thème par défaut du CSS sera utilisé (généralement 'light')

        } catch (error) {
          // En cas d'erreur (localStorage bloqué, etc.), on log discrètement
          // et on laisse le thème par défaut s'appliquer
          console.warn('Impossible de charger la préférence de thème:', error);
        }
      })();
    </script>
    <!-- FIN DU SCRIPT MAGIQUE -->

    <!-- Les feuilles de style sont chargées APRÈS le script -->
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/styles.css">

    <!-- Autres ressources du head -->
    <link rel="icon" href="favicon.ico">
</head>
<body>
    <!-- Contenu de la page -->
</body>
</html>

Anatomia do Script: Cada Linha Conta

Vamos analisar este script linha por linha para entender cada decisão de design:

A IIFE (expressão de função imediatamente invocada)

(function() {
  // ...
})();

Esta estrutura cria uma função que é executada imediatamente. Por quê? Para isolar nossas variáveis em um escopo local e evitar poluir o escopo global. Mesmo que usemos apenas`const`(que tem um escopo de bloco), a IIFE é uma boa prática que deixa nossas intenções claras e protege contra possíveis conflitos de nomes.

O Modo Estrito

'use strict';

Esta diretiva ativa o modo estrito do JavaScript, que : - Proíbe o uso de variáveis não declaradas - Gera erros para as operações perigosas - Melhora o desempenho em alguns motores JavaScript

Para um script crítico como este, queremos a máxima segurança.

O bloco try…​catch

try {
  // Code principal
} catch (error) {
  console.warn('Impossible de charger la préférence de thème:', error);
}

Este bloco é absolutamente crucial. Garante que se algo correr mal (localStorage bloqueado, erro de sintaxe improvável, etc.), o nosso script não bloqueará o carregamento de toda a página. A utilização de`console.warn`em vez de`console.error`indica que é um problema não crítico

A Leitura do localStorage

const savedTheme = localStorage.getItem('preferred-theme');

Esta linha pode lançar uma exceção em alguns contextos (navegação privada rígida do Safari). Por isso, ela está em um bloco try…​catch.

A Aplicação Condicional

if (savedTheme) {
  document.documentElement.setAttribute('data-bs-theme', savedTheme);
}

Aplicamos o tema apenas se encontrarmos um salvo. Caso contrário, deixamos o CSS usar seu tema padrão. Essa abordagem é mais robusta do que um valor padrão fixo no JavaScript.

Detecção da Preferência do Sistema (Bônus)

Uma melhoria opcional, mas elegante, consiste em detectar a preferência de tema do sistema operacional do usuário se ele ainda não fez uma escolha explícita em seu aplicativo:

else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
  document.documentElement.setAttribute('data-bs-theme', 'dark');
}

Esta funcionalidade utiliza a Media Query`prefers-color-scheme`para consultar o sistema. No macOS, Windows 10+, iOS, e Android moderno, esta consulta retorna a preferência do sistema do usuário.

Vantagens : - Experiência personalizada desde a primeira visita - Coerência com o ambiente do sistema do usuário - Nenhum armazenamento necessário para a primeira visita

Considerações : - Nem todos os navegadores suportam esta funcionalidade (mas o suporte é excelente desde 2020) - A verificação`window.matchMedia`garante a compatibilidade - O utilizador pode sempre sobrescrever esta escolha

Desempenho: Por que este script é rápido

Nosso script de pré-carregamento foi projetado para ser extremamente rápido:

tamanho mínimoCerca de 300 bytes não minificados, 200 bytes minificados. É insignificante comparado a qualquer imagem ou biblioteca JavaScript.

Em linha: Sem requisição HTTP adicional. O script está no HTML, portanto está imediatamente disponível.

Operações síncronas simples: Leitura de uma chave no localStorage (operação ultra-rápida) e modificação de um atributo DOM (operação nativa do navegador).

Nenhuma dependência: Sem framework, sem biblioteca, apenas JavaScript puro. Sem tempo de inicialização, sem análise de dependências.

Execução única: Este script é executado uma única vez no carregamento. Sem listeners de eventos, sem loops, sem cálculos complexos.

Na prática, em hardware moderno, esse script é executado em menos de 1 milissegundo, um tempo imperceptível que não tem impacto no desempenho de carregamento da página.

Colocação Óptima no `<head>

A ordem dos elementos no`<head>`É importante. Eis a ordem recomendada:

<head>
    <!-- 1. Métadonnées critiques -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- 2. Notre script de pré-chargement (IMMÉDIATEMENT après les meta) -->
    <script>
      (function() { /* notre code */ })();
    </script>

    <!-- 3. Titre de la page -->
    <title>Mon Site</title>

    <!-- 4. Feuilles de style -->
    <link rel="stylesheet" href="styles.css">

    <!-- 5. Autres ressources (fonts, favicons, etc.) -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="icon" href="favicon.ico">

    <!-- 6. Autres scripts avec defer ou async -->
    <script src="app.js" defer></script>
</head>

Esta ordem garante que :

  1. O charset é definido antes de qualquer processamento de texto

  2. Nosso script é executado antes do carregamento dos CSS

  3. Os CSS são carregados depois e aplicam diretamente o tema correto

  4. Os outros recursos não críticos são carregados por último

Passo 3 : O Poder dos Seletores de Atributos CSS

O sistema de tema do Bootstrap 5

Bootstrap 5 introduziu um sistema elegante de gerenciamento de temas baseado nas propriedades personalizadas CSS (variáveis CSS) e nos seletores de atributos. Este sistema usa o atributo`data-bs-theme`no elemento`<html>`para determinar qual conjunto de variáveis de cor aplicar.

A beleza deste sistema reside na sua simplicidade: em vez de carregar diferentes folhas de estilo ou de alternar as classes em milhares de elementos, alteramos simplesmente um atributo em um único elemento, e o CSS faz o resto graças à cascata.

Estrutura CSS para um Sistema de Tema

Aqui está uma estrutura CSS completa para implementar um sistema de tema robusto:

/**
 * SYSTÈME DE THÈME COMPLET
 * Utilise les Custom Properties CSS pour une maintenance facile
 */

/* ============================================
   THÈME PAR DÉFAUT (LIGHT)
   Défini sur :root pour être le fallback
   ============================================ */
:root {
  /* Couleurs de base */
  --color-primary: #0d6efd;
  --color-secondary: #6c757d;
  --color-success: #198754;
  --color-danger: #dc3545;
  --color-warning: #ffc107;
  --color-info: #0dcaf0;

  /* Couleurs de fond */
  --bg-primary: #ffffff;
  --bg-secondary: #f8f9fa;
  --bg-tertiary: #e9ecef;

  /* Couleurs de texte */
  --text-primary: #212529;
  --text-secondary: #6c757d;
  --text-tertiary: #adb5bd;

  /* Couleurs de bordure */
  --border-color: #dee2e6;
  --border-color-subtle: #e9ecef;

  /* Couleurs d'ombre */
  --shadow-sm: rgba(0, 0, 0, 0.075);
  --shadow-md: rgba(0, 0, 0, 0.15);
  --shadow-lg: rgba(0, 0, 0, 0.25

Articles connexes