← Biblioteca componentes/efeitos/moldura-secao/ficha.md CódigoTela cheia
efeitos/moldura-secao

Moldura de seção

🟢 estavel complexidade baixa prefixo frame- moldura quebra transicao-de-secao tom-vizinho gradiente cercadura css-puro sem-js decoracao sem javascript

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

<link rel="stylesheet" href="componentes/efeitos/moldura-secao/estilo.css">

markup.html · 36 linhas · 2,2 KB

<!-- ==========================================================================
     MOLDURA DE SEÇÃO — a dobra de tom vizinho, dissolvida nas bordas
     Prefixo: frame-
     Requer: core/tokens.css, core/base.css, core/motion.css, core/motion.js
             + este estilo.css. Sem script.

     O componente é a JUNÇÃO, não a seção: ele não tem o que dissolver sem uma
     vizinha escura em cima e outra embaixo. No site real essas vizinhas são as
     dobras que você já tem; no preview elas vivem em preview-antes.html e
     preview-extra.html, para que este arquivo seja só o que se cola.

     E o que se cola é pouco: uma classe na seção (.frame) e dois <div> vazios
     de cercadura (.frame-edge), que são opcionais. Só a classe já entrega a
     dissolução.
     ========================================================================== -->

<!-- ===================== COMPONENTE — INÍCIO ===================== -->

<!-- A classe .frame entra na SEÇÃO, junto do escopo de tom e do .sec-pad.
     Vizinhas em outro tom? Acrescente style="--frame-tone:var(--dark-2);--frame-tone-rgb:19,19,19". -->
<section class="sec-deep sec-pad frame" id="moldura">

  <!-- Cercadura: dois <div> vazios, um por borda. São decoração pura e
       podem ser apagados — a dissolução continua. Nunca ganham conteúdo. -->
  <div class="frame-edge frame-edge--top" aria-hidden="true"></div>
  <div class="frame-edge frame-edge--bottom" aria-hidden="true"></div>

  <div class="wrap">
    <span class="eyebrow" data-rv>Dobra emoldurada</span>
    <h2 class="sec-title ttl">Destacar uma dobra sem <em>trocar</em> de tom</h2>
    <p class="sec-lead" data-rv data-d="1">O fundo desta seção é meio ponto mais claro que o das vizinhas — diferença pequena demais para virar corte, e é a moldura que a torna visível. As faixas dissolvem as bordas no tom de fora; a cercadura chanfrada marca onde a dobra começa e onde termina.</p>
    <p class="sec-lead" data-rv data-d="2">O conteúdo aqui dentro é livre: o componente é só a borda. Cabe o hub, o FAQ, a lista longa — o que precisa de moldura, não de contraste.</p>
  </div>
</section>

<!-- ====================== COMPONENTE — FIM ====================== -->

estilo.css · 157 linhas · 7,3 KB

/* ==========================================================================
   MOLDURA DE SEÇÃO — a dobra de tom vizinho, dissolvida nas bordas
   Prefixo: frame-
   Tokens usados: --bg --dark --dark-rgb --rule-2 --accent-2 --pad --max
   (a seção continua lendo o próprio escopo de tom; este componente nunca
   toca em --fg nem redefine --bg)

   Duas peças independentes, e é assim de propósito:

     .frame        a seção. Duas faixas de gradiente nas bordas, feitas de
                   camadas de background. Zero markup — é só a classe.
     .frame-edge   a cercadura chanfrada. Um <div> VAZIO por borda, opcional.

   POR QUE A FAIXA É BACKGROUND E NÃO ::before/::after
   A origem usava os dois pseudo-elementos da seção para as faixas, e por
   isso precisava de duas muletas: overflow:hidden na seção (que quebra
   position:sticky de qualquer componente colado dentro) e z-index:2 no
   .wrap — ou seja, o componente estilizava a primitiva do grid, o que o
   contrato proíbe. Camada de background resolve os dois de graça: fundo é
   fundo, sempre atrás do conteúdo, sem stacking context e sem clipe. De
   quebra, os dois pseudo-elementos da seção ficam livres para quem for
   colar outro componente no mesmo <section>.

   POR QUE A CERCADURA AINDA É MARKUP
   Ela tem 5 marcas por borda: dois chanfros, dois filetes e o losango do
   meio. Pseudo-elemento não tem pseudo-elemento, então a alternativa a um
   nó vazio seria empilhar 8 camadas de gradiente com listas de size e
   position casadas na unha — ilegível e frágil. Um <div> vazio com
   aria-hidden é mais barato de manter e de entender. A origem gastava 7 nós
   por borda (wrap + 2 SVG + 2 filetes + losango); aqui é 1, sem filhos.
   ========================================================================== */

/* ==========================================================================
   1. A FAIXA — a dissolução
   ==========================================================================
   Knobs:
     --frame-band      altura da faixa (as duas de uma vez)
     --frame-band-top  altura só da de cima  (0 = borda seca)
     --frame-band-bot  altura só da de baixo (0 = borda seca)
     --frame-tone      cor da vizinha, para onde a borda dissolve
     --frame-tone-rgb  a MESMA cor em canal separado, para o fim transparente

   --frame-tone e --frame-tone-rgb andam em par: o fim do gradiente tem de
   ser a mesma cor com alfa 0, senão a faixa passa por um cinza no meio do
   caminho. Trocou um, troque o outro. */
.frame{
  --frame-band:clamp(80px,13vh,180px);
  --frame-band-top:var(--frame-band);
  --frame-band-bot:var(--frame-band);
  --frame-tone:var(--dark);
  --frame-tone-rgb:var(--dark-rgb);

  /* a cercadura é absoluta e ancora aqui. O core já dá position:relative em
     <section>; repetir garante que a moldura também funcione num <div>. */
  position:relative;

  /* Mesmo motivo: sem fundo próprio, a faixa pinta escuro sobre escuro e o
     efeito some sem erro nenhum. O valor é o do escopo de tom da seção —
     não há redefinição de --bg aqui. */
  background-color:var(--bg);

  background-image:
    linear-gradient(to bottom,var(--frame-tone) 0%,rgba(var(--frame-tone-rgb),0) 100%),
    linear-gradient(to top,   var(--frame-tone) 0%,rgba(var(--frame-tone-rgb),0) 100%);
  background-repeat:no-repeat;
  background-size:100% var(--frame-band-top),100% var(--frame-band-bot);
  background-position:top,bottom;
}

/* Dissolve só uma das bordas. Zerar a altura do outro lado é o que mantém o
   par de gradientes único — não existe uma segunda declaração por variação. */
.frame--top{--frame-band-bot:0px}
.frame--bottom{--frame-band-top:0px}

/* ==========================================================================
   2. A CERCADURA — o acabamento chanfrado
   ==========================================================================
   Knobs:
     --frame-cap   lado do chanfro (é a altura da tira também)
     --frame-gap   meio vão em volta do losango, do centro para fora
     --frame-dm    lado do losango (0 esconde; zere o --frame-gap junto)

   As cores não têm knob de propósito: o filete lê --rule-2 e o losango lê
   --accent-2, os dois vindos do escopo de tom. É o que faz a mesma cercadura
   funcionar numa .sec-deep e numa .sec-paper sem uma linha de ajuste. */
.frame-edge{
  --frame-cap:30px;
  --frame-gap:17px;   /* 14px de respiro + metade do losango */
  --frame-dm:6px;

  position:absolute;z-index:2;pointer-events:none;
  height:var(--frame-cap);

  /* A tira ocupa exatamente a coluna. É a conta do .wrap refeita aqui, em
     inset — o componente não encosta na primitiva do grid nem herda o
     padding dela: quando a tela é menor que --max, a coluna começa em --pad;
     acima disso, na margem que centraliza --max. */
  left:max(var(--pad),calc(50% - var(--max)/2 + var(--pad)));
  right:max(var(--pad),calc(50% - var(--max)/2 + var(--pad)));

  /* Quatro camadas: chanfro esquerdo, chanfro direito, filete esquerdo,
     filete direito. Os chanfros são gradientes a 45° e não SVG porque
     data-URI não enxerga var() — um SVG inline cravaria a cor da linha no
     componente, e a cor tem de vir do escopo de tom. Em caixa quadrada o
     "to top right" cai exatamente na diagonal, com 1px de espessura. */
  background-repeat:no-repeat;
  background-image:
    linear-gradient(to top right,
      transparent calc(50% - .5px),var(--rule-2) calc(50% - .5px),
      var(--rule-2) calc(50% + .5px),transparent calc(50% + .5px)),
    linear-gradient(to bottom right,
      transparent calc(50% - .5px),var(--rule-2) calc(50% - .5px),
      var(--rule-2) calc(50% + .5px),transparent calc(50% + .5px)),
    linear-gradient(var(--rule-2),var(--rule-2)),
    linear-gradient(var(--rule-2),var(--rule-2));
  background-size:
    var(--frame-cap) var(--frame-cap),
    var(--frame-cap) var(--frame-cap),
    calc(50% - var(--frame-cap) - var(--frame-gap)) 1px,
    calc(50% - var(--frame-cap) - var(--frame-gap)) 1px;
  background-position:
    left top,
    right top,
    left var(--frame-cap) bottom 0,
    right var(--frame-cap) bottom 0;
}

/* O losango do meio: quadrado girado, centrado no fio do filete.
   -3px o sobe para o centro do próprio corpo, +.5px o desce para o meio da
   linha de 1px. Só ele justifica um pseudo-elemento aqui — as outras quatro
   marcas cabem em camada de background, esta não: forma girada não existe
   em gradiente sem virar quatro triângulos. */
.frame-edge::before{
  content:"";position:absolute;left:50%;bottom:0;
  width:var(--frame-dm);height:var(--frame-dm);
  margin-left:calc(var(--frame-dm) / -2);
  margin-bottom:calc(var(--frame-dm) / -2 + .5px);
  background:var(--accent-2);
  transform:rotate(45deg);
}

.frame-edge--top{top:0}

/* A borda de baixo é a de cima espelhada. scaleY(-1) na tira inteira faz o
   filete subir para o topo dela e os chanfros mergulharem para os cantos,
   sem uma segunda lista de gradientes. O losango girado espelhado continua
   sendo o mesmo losango. */
.frame-edge--bottom{bottom:0;transform:scaleY(-1)}

/* ---- mobile ----
   Abaixo de 640px a coluna fica estreita e o vão em volta do losango come
   metade do filete: a cercadura passa a ler como quatro riscos soltos.
   Some com o losango e feche o vão — os dois filetes viram uma linha só. */
@media(max-width:640px){
  .frame-edge{--frame-gap:0px;--frame-dm:0px}
  .frame-edge::before{display:none}
}

Uma dobra de tom vizinho — .sec-deep entre duas .sec-dark — com as bordas dissolvidas. Duas faixas de gradiente, no topo e na base, pintam o tom das seções de fora e vão a zero para dentro: onde deveria haver uma emenda entre dois pretos parecidos, não há nada. Por cima, uma cercadura chanfrada marca onde a dobra começa e onde termina — dois chanfros de 30px mergulhando para os cantos, dois filetes e um losango de 6px no meio.

O componente não é a seção: é a junção. Ele não sabe nem quer saber o que está dentro.


Por que ele existe — §3 do contrato

O CLAUDE.md §3 fecha uma porta:

Fundos escuros adjacentes têm de ser quase idênticos (#0a0a0a / #131313). Salto forte de tom entre seções suja a página.

A regra está certa e cria um problema imediato: se a diferença entre a dobra e as vizinhas é quase imperceptível, como fazer a dobra ler como bloco à parte? As duas saídas óbvias são proibidas ou feias — subir o contraste desobedece à regra, e traçar uma linha reta na borda transforma a página em tabela.

Esta é a terceira saída, e a resposta canônica da biblioteca a essa regra: mantenha a diferença de tom minúscula, apague as duas emendas com um degradê que devolve o tom de fora, e marque o início e o fim com um acabamento fino. A dobra passa a ser emoldurada em vez de cortada. O olho registra "outro bloco" sem registrar "outra cor".

Quando o que você precisa é contraste de verdade, o §3 já responde por outro caminho: uma .sec-paper inteira como respiro — e aí o componente é o efeitos/quebra-papel-rasgado.


Quando usar

Quando não usar


Padrão editorial

Este componente não tem slot de texto — o que ele pede é arranjo de tons, e é isso que a tabela governa:

Slot Regra
Seção emoldurada Escopo de tom vizinho ao das seções de fora: .sec-deep (--dark-2) entre .sec-dark (--dark). Nunca um terceiro tom inventado.
Vizinha de cima Tem de terminar exatamente no tom que a faixa pinta (--frame-tone). Se ela for .sec-deep, troque o par de tom.
Vizinha de baixo Idem. A moldura fica torta se as duas vizinhas tiverem tons diferentes — nesse caso, use .frame--top ou .frame--bottom e emoldure um lado só.
Conteúdo interno Livre, e sempre dentro do .wrap. Título de seção, olho e apoio entram pelas primitivas do core, como em qualquer dobra.
Cercadura Sempre vazia. É decoração com aria-hidden — nem texto, nem ícone, nem número.
Quantidade 2 a 3 dobras emolduradas por página. A quarta anula o efeito de destaque das três primeiras.

Geometria

Medida Valor Por quê
Altura da faixa clamp(80px,13vh,180px) Some antes de encostar no conteúdo: sempre abaixo de --sec-gap (clamp(110px,14vh,160px)).
Chanfro 30px É também a altura da tira da cercadura. Abaixo de 20px o chanfro vira degrau; acima de 40px vira moldura de quadro.
Filete 1px, em --rule-2 Linha divisória forte do escopo. Em .sec-deep é #404040 — visível e sem brilhar.
Losango 6px, em --accent-2 O único ponto de cor da peça. É acento secundário de propósito: acento primário aqui compete com o botão da seção.
Vão do losango 17px de cada lado 14px de respiro + metade do losango. Some abaixo de 640px.

Como usar

<link rel="stylesheet" href="componentes/efeitos/moldura-secao/estilo.css">

Sem script. São duas peças, e a segunda é opcional:

<section class="sec-deep sec-pad frame">
  <div class="frame-edge frame-edge--top" aria-hidden="true"></div>
  <div class="frame-edge frame-edge--bottom" aria-hidden="true"></div>
  <div class="wrap">…</div>
</section>

Só a classe .frame já entrega a dissolução — os dois <div> acrescentam o acabamento chanfrado. As vizinhas continuam sendo as seções que você já tem; nada é colado nelas.

Ganchos

Gancho Papel
.frame na seção. As duas faixas de gradiente
.frame--top / .frame--bottom dissolve só uma das bordas
.frame-edge + --top / --bottom a cercadura. <div> vazio, aria-hidden, um por borda
--frame-band altura das duas faixas (--frame-band-top / --frame-band-bot para uma só)
--frame-tone / --frame-tone-rgb o tom da vizinha, em par: cor e canal RGB da mesma cor
--frame-cap lado do chanfro, no .frame-edge
--frame-gap meio vão em volta do losango
--frame-dm lado do losango (0 esconde)

Variações

Variação Como
Só uma borda .frame--top ou .frame--bottom
Sem cercadura apague os dois <div>
Cercadura sem faixa .frame-edge numa seção sem .frame
Vizinhas em outro tom style="--frame-tone:var(--dark-2);--frame-tone-rgb:19,19,19"
Faixa mais curta --frame-band:90px
Sem losango --frame-dm:0;--frame-gap:0 — o filete vira inteiriço

As cores não têm knob, e é de propósito: o filete lê --rule-2 e o losango lê --accent-2, os dois vindos do escopo de tom. É o que faz a mesma cercadura funcionar numa .sec-deep e numa .sec-paper sem uma linha de ajuste.


Gotchas


Histórico

Projeto Onde Data Diferenças
Liberta Wealth site/assets/css/style.css (.framed, .edge) usadas por #planejamento e #duvidas 2026-08 Origem. Faixas em ::before/::after com overflow:hidden e z-index no .wrap; cercadura em 7 nós por borda, com dois SVG inline; classes sem prefixo (.framed, .edge, .cap, .dm) e um seletor de elemento (.edge i); losango em --tan.

Calibragem no primeiro uso. Ressalva de proveniência: a regra só existe na composição site/ da Liberta Wealth, cuja ida ao ar não está confirmada — na versão confirmada (github-repo/, o design system v5 que a biblioteca varreu) nem .framed nem .edge existem. Foi construído em casa, então não há autoria nem licença externa a creditar.

Normalizações aplicadas na extração:

Na origem Aqui Por quê
.framed, .edge, .cap, .dm .frame, .frame-edge, modificadores --top/--bottom §6.1: um prefixo, sempre. .edge e .cap colidem com qualquer projeto que junte dois componentes
.edge i{…} camada de background §6.3: zero seletor de elemento dentro de componente
.framed > .wrap{position:relative;z-index:2} não existe mais §4: o componente não encosta na primitiva do grid. Sem pseudo-elemento por cima, não há o que empurrar para baixo
.framed{overflow:hidden} não existe mais Corte na seção quebra position:sticky de componentes colados dentro
.edge .wrap como régua inset com a conta de --pad/--max Mesma razão: a coluna é do .wrap, e o componente não a reaproveita como estrutura
Faixas em ::before/::after camadas de background-image Fundo é fundo. Libera os dois pseudo-elementos da seção para quem colar outro componente no mesmo <section>
2 SVG inline por borda, com currentColor gradientes a 45° data: URI não lê var(); a cor tem de vir do escopo de tom
--tan cravado no losango --accent-2 §3: componente lê token, não cor de projeto
14 nós decorativos por seção 2 <div> vazios Markup decorativo repetido é ruído para quem cola o snippet
Altura da faixa e do chanfro cravadas --frame-band, --frame-cap, --frame-gap, --frame-dm Ajuste sem reescrever regra

A decisão de arquitetura: pseudo-elemento onde cabe, markup onde acabou

A origem repetia 14 nós de decoração por seção (dois .edge, cada um com .wrap, dois SVG, dois filetes e o losango). A pergunta na extração era se dava para zerar isso.

Deu, para a metade que importa. As faixas — o efeito essencial, a resposta ao §3 — não têm markup nenhum: são duas camadas de background da própria seção. Quem quiser só a dissolução escreve uma classe e acabou.

Não deu para a cercadura, e o motivo é estrutural: ela tem 5 marcas por borda (dois chanfros, dois filetes, um losango) e pseudo-elemento não tem pseudo-elemento. Os dois pseudos da seção já seriam poucos para as duas bordas; e o losango, sendo forma girada, não existe em camada de background sem virar quatro triângulos de gradiente. Zerar o markup custaria oito camadas por borda com listas de background-size e background-position casadas na unha — ilegível, frágil, e impossível de ajustar sem recontar posições.

O saldo: 1 nó vazio por borda, sem filhos, contra 7. O <div> vazio com aria-hidden é mais barato de manter e de entender do que a alternativa "elegante", e tem um bônus que a versão sem markup não teria: a cercadura passa a ser peça independente — dá para usar a faixa sem ela, ela sem a faixa, ou só numa das bordas.

Divergências conhecidas vs. produção: nenhuma verificável — a composição de origem não está publicada. O desenho (30px de chanfro, 6px de losango, clamp(80px,13vh,180px) de faixa) é o da origem, medida por medida.