← Biblioteca componentes/navegacao/progresso-leitura/ficha.md CódigoTela cheia
navegacao/progresso-leitura

Progresso de leitura

🟢 estavel complexidade baixa prefixo prog- progresso scroll indicador fixo topo leitura artigo navegacao

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/navegacao/progresso-leitura/estilo.css">
<script src="componentes/navegacao/progresso-leitura/script.js"></script>

markup.html · 36 linhas · 1,9 KB

<!-- ==========================================================================
     PROGRESSO DE LEITURA — filete no topo que acompanha a rolagem
     Prefixo: prog-
     Requer: core (tokens, base, motion.css, motion.js) + este estilo.css e
             este script.js

     NÃO É UMA DOBRA. É um elemento de PÁGINA: um filete de 3px colado no
     topo da janela que cresce da esquerda para a direita conforme a pessoa
     desce. Vive uma vez por página, como primeiro filho do <body>, e não
     tem nada a ver com a seção onde este markup for colado.

     ONDE COLAR: logo depois da abertura do <body>, antes de qualquer
     seção. A posição no DOM não muda o desenho (é position:fixed), mas
     muda a ordem de leitura — e um indicador decorativo tem de vir antes
     do conteúdo, nunca no meio dele.

     FORA DE QUALQUER [data-rv] — isto é medido, não estilo. O core termina
     todo reveal em `filter:blur(0)`, e blur(0) continua sendo um FILTRO:
     um elemento com filtro vira bloco de contenção para descendente
     `position:fixed`. Se este filete nascesse dentro de uma dobra revelada,
     ele grudaria no topo da DOBRA em vez do topo da JANELA, e o sintoma
     apareceria só depois que a seção entrasse na tela — o pior tipo de
     bug, o que não acontece na primeira olhada. Mesma razão pela qual o
     listas/lista-imagem-flutuante mantém a imagem que segue o cursor fora
     do data-rv.

     aria-hidden DE PROPÓSITO. O filete não informa nada que o leitor de
     tela já não saiba melhor: posição no documento é coisa que a tecnologia
     assistiva reporta com precisão, e um `role="progressbar"` aqui só
     acrescentaria um anúncio a cada scroll. Ele é ornamento de quem enxerga
     a página inteira de uma vez.
     ========================================================================== -->

<div class="prog-rail" data-prog aria-hidden="true">
  <span class="prog-fill"></span>
</div>

estilo.css · 52 linhas · 2,1 KB

/* ==========================================================================
   PROGRESSO DE LEITURA — prog-
   Filete no topo da janela que cresce conforme a pagina desce.
   Requer core/tokens.css. O script escreve --prog-k (0 a 1) no proprio nó.
   ========================================================================== */

.prog-rail{
  position:fixed;
  inset-block-start:var(--prog-top, 0px);
  inset-inline:0;
  height:var(--prog-h, 3px);
  /* Acima do menu fixo, que para no 100. Um indicador que some atras do
     header nao indica nada. */
  z-index:var(--prog-z, 120);
  background:var(--rule);
  /* Nunca rouba clique: e' uma faixa da largura da tela colada no topo,
     bem onde mora o primeiro item de menu de muito site. */
  pointer-events:none;
  /* Sem isto o filete some quando a primeira secao e' clara: --rule e
     --accent sao tokens de ESCOPO DE TOM, e o :root serve o escuro. Ancorar
     ao escopo do topo da pagina e' decisao do consumidor -- ver a variacao
     "sobre secao clara" na ficha. */
  isolation:isolate;
}

.prog-fill{
  display:block;
  height:100%;
  width:100%;
  background:var(--accent);
  /* scaleX, nao width: transform e opacidade sao as duas unicas propriedades
     que o navegador anima sem refazer layout. Com width, cada quadro de
     rolagem remontaria a caixa -- em pagina longa isso aparece como filete
     tremido no celular. */
  transform:scaleX(var(--prog-k, 0));
  transform-origin:0 50%;
  /* Suavizacao curta: o scroll ja' chega em passos, e sem isto o filete
     "pula" a cada entalhe de roda de mouse. 90ms e' o teto do que se
     percebe como resposta imediata. */
  transition:transform 90ms linear;
}

/* Pagina que nao rola nao tem progresso a mostrar. O script marca isto
   quando scrollHeight nao passa da altura da janela -- ver o gotcha da
   divisao por zero na ficha. */
.prog-rail[hidden]{display:none}

@media (prefers-reduced-motion: reduce){
  /* A POSICAO continua sendo informada: ela e' dado, nao enfeite. O que sai
     e' so' a suavizacao entre um valor e o seguinte. */
  .prog-fill{transition:none}
}

script.js · 48 linhas · 2,0 KB

/* ==========================================================================
   PROGRESSO DE LEITURA — prog-
   Le a posicao da pagina e escreve --prog-k (0 a 1) no proprio filete.
   Nao anima nada: quem suaviza e' a transition de 90ms do estilo.css, que
   o @media(prefers-reduced-motion) do proprio componente desliga. Por isso
   nao ha consulta a LIB.reduce aqui -- nao existe movimento em JS para
   desligar, e movimento reduzido ja' esta' tratado no CSS.
   ========================================================================== */
(function () {
  var rail = document.querySelector('[data-prog]');
  if (!rail) return;                       // guard: sem o filete, saida silenciosa

  var fill = rail.querySelector('.prog-fill');
  if (!fill) return;

  var doc = document.documentElement;

  function medir() {
    // O denominador e' o quanto a pagina TEM para rolar, nao a altura dela.
    // Em pagina curta isso da' zero, e zero no divisor devolve NaN -- que o
    // CSS descarta em silencio, deixando o filete cheio para sempre. Por
    // isso a pagina curta esconde o indicador em vez de mostrar um errado:
    // nao ha progresso a indicar quando nao ha o que percorrer.
    var curso = doc.scrollHeight - innerHeight;
    if (curso <= 0) {
      rail.hidden = true;
      return;
    }
    rail.hidden = false;

    var k = (window.pageYOffset || doc.scrollTop) / curso;
    // Rolagem elastica (o "puxao" do iOS e do trackpad) devolve valor abaixo
    // de zero no topo e acima de um no fim. Sem o grampo, o filete encolhe
    // para dentro da esquerda ou vaza pela direita durante o repique.
    if (k < 0) k = 0;
    if (k > 1) k = 1;

    rail.style.setProperty('--prog-k', k.toFixed(4));
  }

  // LIB.onScroll e' o laco unico do core, em requestAnimationFrame: um so'
  // para a pagina inteira, em vez de um listener por componente. Ele ja'
  // dispara no resize, o que cobre a rotacao de tela e a barra de endereco
  // do navegador movel entrando e saindo -- os dois mudam innerHeight e,
  // com ele, o denominador.
  LIB.onScroll(medir);
  medir();
})();

Um filete de 3px colado no topo da janela que cresce da esquerda para a direita conforme a página desce, e fecha a largura inteira quando ela acaba. Não é uma dobra: é um elemento de página, que vive uma vez, como primeiro filho do <body>, e não pertence a nenhuma seção.

Ele resolve um problema de página longa: a barra de rolagem do navegador desapareceu do celular e encolheu no desktop, e sem ela a pessoa não sabe se falta um parágrafo ou dez. O filete devolve essa informação sem ocupar espaço de layout.


Quando usar

Quando não usar


Padrão editorial

Não há conteúdo a escrever: o componente não tem texto. O que existe é uma decisão de cor, e ela é editorial.

O filete lê --accent. Numa página em que o acento já é o botão, o olho e o realce, ele vira o quarto uso da mesma cor — e aí a cor perde a função de apontar. Em página assim, ancore o filete em --fg-muted ou --rule-2 e deixe o acento para o que converte.


Especificação dos assets

Nenhum. Não há imagem, ícone nem SVG.


Como usar

Cole o markup logo depois da abertura do <body>, antes de qualquer seção:

<body>
  <div class="prog-rail" data-prog aria-hidden="true">
    <span class="prog-fill"></span>
  </div>
  <!-- o resto da página -->

A posição no DOM não muda o desenho — é position:fixed —, mas muda a ordem de leitura, e um ornamento tem de vir antes do conteúdo, nunca no meio dele.

Ganchos

Gancho O quê
[data-prog] a raiz que o script procura. Um por página
--prog-k 0 a 1, escrito pelo script no próprio nó. O CSS lê no scaleX
--prog-top distância do topo. 0px por padrão; var(--nav-h) desce para baixo do menu
--prog-h espessura. 3px
--prog-z camada. 120, acima do menu fixo, que para no 100

Variações

Variação Como
Abaixo do menu fixo --prog-top: var(--nav-h). O filete encosta na borda de baixo do header em vez de cobrir o topo dele
Sobre seção clara o trilho lê --rule e --accent do :root, que serve o escuro. Em página de topo claro, declare o escopo no próprio filete (class="prog-rail sec-paper") ou fixe a cor no tema
Mais grosso --prog-h: 5px. Acima disso deixa de ser filete e vira barra, competindo com o header

Gotchas


Movimento reduzido / toque

Com prefers-reduced-motion: reduce, o filete continua funcionando: a posição é dado, não enfeite. O que sai é só a suavização de 90ms entre um valor e o seguinte, e a barra passa a saltar direto para o valor certo.

No toque não há nada a adaptar — não depende de ponteiro. A barra de endereço do navegador móvel entrando e saindo muda innerHeight, e com ele o denominador; LIB.onScroll já dispara no resize, então a conta se refaz sozinha.


Histórico

Origem Quando O quê
autoral 2026-09-04 Construído do zero. Padrão de domínio comum, sem referência única

Origem. É o filete do Medium, dos blogs de produto e de quase toda página longa premiada no Awwwards — um padrão tão difundido que não tem dono. Nada foi copiado: reescrito do zero dentro do contrato desta biblioteca. O registro consolidado vai em projetos/autorais.md.

Decisões da tradução para o contrato. Cresce por transform:scaleX e não por width, porque width remonta a caixa a cada quadro. O valor entra por --prog-k, custom property do próprio componente, o que deixa a suavização inteira no CSS e o script sem uma linha de animação — por isso ele não consulta LIB.reduce: não há movimento em JS para desligar, e o @media do próprio estilo.css já trata. A página curta esconde o indicador em vez de mostrar um cheio. O valor é grampeado por causa da rolagem elástica. E o filete é aria-hidden de propósito: posição no documento é justamente o que a tecnologia assistiva já reporta melhor que qualquer barra — um role="progressbar" aqui só acrescentaria um anúncio a cada scroll.

Calibragem no primeiro uso. Espere decidir a cor: --accent é o padrão, mas em página onde o acento já é botão, olho e realce, o filete vira o quarto uso da mesma cor. Ver o padrão editorial acima.