← Biblioteca componentes/efeitos/cursor-anotacao/ficha.md CódigoTela cheia
efeitos/cursor-anotacao

Legenda de cursor

🟢 estavel complexidade baixa prefixo cur- cursor anotacao bolha rotulo camada sobreposicao ponteiro movimento-continuo decoracao saas

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

<link rel="stylesheet" href="componentes/efeitos/cursor-anotacao/estilo.css">
<script src="componentes/efeitos/cursor-anotacao/script.js"></script>

markup.html · 65 linhas · 3,1 KB

<!-- ==========================================================================
     LEGENDA DE CURSOR — bolha ancorada com ponteiro, deslizando entre pontos
     Prefixo: cur-
     Requer: core/tokens.css, core/base.css, core/motion.css, core/motion.js
             + este estilo.css e este script.js

     O componente é a CAMADA, não a seção: ele se sobrepõe ao que já existe.
     A <section> abaixo é CONTEXTO — no site real ela é a dobra que você já
     tem, e você não a cola daqui. O que se copia é a .cur-layer e as notas
     dentro dela.

     A camada é absoluta e cobre a seção inteira. O core já dá
     position:relative a toda <section>, então não há nada a preparar: basta
     a camada ser filha direta da seção (ou de qualquer caixa relativa).

     Cada nota anda entre as âncoras do data-cur-pos, em porcentagem da
     camada, na ordem escrita e em laço. Uma âncora só = nota parada.
     ========================================================================== -->

<!-- CONTEXTO — a dobra por baixo. Não faz parte do componente. -->
<section class="sec-light sec-pad" id="cursor">

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

  <div class="cur-layer">

    <!-- Cauda embaixo à direita: o canto quadrado da bolha é onde o
         ponteiro encosta. Duas âncoras = vai e volta. -->
    <span class="cur-note cur-note-br cur-note-deep"
          data-cur-note
          data-cur-pos="5% 4%; 17% 4%"
          data-cur-hold="2600">
      <span class="cur-body" data-rv>
        <span class="cur-tip" aria-hidden="true">
          <svg viewBox="0 0 22 26" fill="none"><path d="M1 1L1 19L6.2 14.2L9.6 22.4L12.8 21L9.4 13L17.4 12.6Z" fill="currentColor"/></svg>
        </span>
        <span class="cur-bubble">Carteira desenhada para o seu horizonte, não para a meta do banco</span>
      </span>
    </span>

    <!-- Cauda em cima à esquerda, na cor de marca cheia. O tempo de parada
         é diferente do da outra de propósito: em passo igual as duas viram
         um metrônomo, e o olho percebe o truque. -->
    <span class="cur-note cur-note-tl"
          data-cur-note
          data-cur-pos="44% 74%; 33% 78%"
          data-cur-hold="3100">
      <span class="cur-body" data-rv data-d="2">
        <span class="cur-tip" aria-hidden="true">
          <svg viewBox="0 0 22 26" fill="none"><path d="M1 1L1 19L6.2 14.2L9.6 22.4L12.8 21L9.4 13L17.4 12.6Z" fill="currentColor"/></svg>
        </span>
        <span class="cur-bubble">Relatório consolidado de todas as instituições</span>
      </span>
    </span>

  </div>

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

  <div class="wrap sec-head">
    <span class="eyebrow" data-rv>Camada de anotação</span>
    <h2 class="sec-title ttl">A dobra por baixo não sabe que <em>existe</em> uma legenda</h2>
    <p class="sec-lead" data-rv data-d="1">A camada é absoluta, não intercepta clique e não entra no fluxo. Qualquer seção serve de base: hero, grade de cards, faixa de prova. O que ela precisa é de espaço vazio onde a bolha caiba sem cobrir texto.</p>
  </div>
</section>

estilo.css · 141 linhas · 5,2 KB

/* ==========================================================================
   LEGENDA DE CURSOR — bolha ancorada com ponteiro
   Prefixo: cur-
   Tokens usados: --accent --dark --light --sans --dur --ease
   ========================================================================== */

/* ---- a camada ----
   Cobre a seção e não intercepta nada. z-index 3 fica acima do .grain (1) e
   do corpo de uma dobra com mídia (2), que é a convenção do resto da casa. */
.cur-layer{position:absolute;inset:0;pointer-events:none;z-index:3}

/* ---- a nota ----
   Duas caixas, e a divisão importa:

     .cur-note  posiciona e desliza. O script só escreve --cur-x/--cur-y aqui.
     .cur-body  é o que ENTRA. É nela que vai o data-rv.

   Separar as duas não é preciosismo: [data-rv] declara `transition` para
   opacity, filter e transform, e o shorthand não se soma. Se o deslize
   morasse no mesmo elemento, o componente carregaria depois do core e
   apagaria a transição do reveal — a nota apareceria seca, contra o §5.

   E o deslize é em left/top, não em transform, porque a âncora é
   porcentagem DA CAMADA: translate em % lê a caixa do próprio elemento,
   não a do pai, então não serve. São dois elementos pequenos andando uma
   vez a cada 2,6s — o custo de não estar no compositor é teórico aqui, e a
   alternativa era calcular pixels no script para escrever um FLIP à mão. */
.cur-note{
  --cur-skin:var(--accent);
  --cur-ink:var(--light);
  --cur-radius:10px;
  --cur-max:212px;
  /* Quanto o ponteiro entra na bolha. Zero encosta a ponta no canto e lê
     como dois adesivos separados; a mordida costura os dois num objeto. */
  --cur-bite:8px;

  position:absolute;
  left:var(--cur-x,0%);
  top:var(--cur-y,0%);
  width:max-content;
  max-width:var(--cur-max);
  transition:left var(--dur) var(--ease),top var(--dur) var(--ease);
}
@media(prefers-reduced-motion:reduce){
  .cur-note{transition:none}
}

.cur-body{position:relative;display:block}

/* ---- a bolha ---- */
.cur-bubble{
  display:block;
  background:var(--cur-skin);
  color:var(--cur-ink);
  font-family:var(--sans);
  font-weight:600;
  font-size:12px;
  line-height:1.35;
  letter-spacing:.005em;
  padding:9px 12px;
  border-radius:var(--cur-radius);
}

/* ---- o ponteiro ----
   Desenho próprio, não um glifo de fonte: cursor de sistema não existe como
   caractere confiável e um ícone de biblioteca traria licença atrás.
   Nasce apontando para cima e à esquerda; cada canto o gira. */
.cur-tip{
  position:absolute;
  width:22px;height:26px;
  color:var(--cur-skin);
}
.cur-tip svg{display:block;width:100%;height:100%}

/* ---- os quatro cantos ----
   O canto QUADRADO da bolha é onde o ponteiro encosta. É a regra inteira:
   quem lê o markup descobre a direção pelo modificador, sem contar pixel. */
.cur-note-tl .cur-bubble{border-top-left-radius:0}
.cur-note-tl .cur-tip{
  left:0;top:0;
  transform:translate(calc(-100% + var(--cur-bite)),calc(-100% + var(--cur-bite)));
}

.cur-note-tr .cur-bubble{border-top-right-radius:0}
.cur-note-tr .cur-tip{
  right:0;top:0;
  transform:translate(calc(100% - var(--cur-bite)),calc(-100% + var(--cur-bite))) rotate(90deg);
}

.cur-note-br .cur-bubble{border-bottom-right-radius:0}
.cur-note-br .cur-tip{
  right:0;bottom:0;
  transform:translate(calc(100% - var(--cur-bite)),calc(100% - var(--cur-bite))) rotate(180deg);
}

.cur-note-bl .cur-bubble{border-bottom-left-radius:0}
.cur-note-bl .cur-tip{
  left:0;bottom:0;
  transform:translate(calc(-100% + var(--cur-bite)),calc(100% - var(--cur-bite))) rotate(270deg);
}

/* ---- variação de tom ----
   A segunda nota da página precisa se distinguir da primeira sem sair da
   marca. Escurecer o próprio --accent contra o --dark mantém as duas na
   mesma família — dois roxos, não roxo e azul. */
.cur-note-deep{--cur-skin:color-mix(in srgb,var(--accent) 62%,var(--dark))}

/* ---- variação de porte ----
   Bolha de canto de tela, em dobra cheia, onde 12px desaparece. */
.cur-note-lg{--cur-max:260px;--cur-radius:12px}
.cur-note-lg .cur-bubble{font-size:13px;padding:11px 14px}
.cur-note-lg .cur-tip{width:26px;height:31px}

/* ==========================================================================
   MOBILE
   A nota vive de espaço vazio, e espaço vazio é a primeira coisa que uma
   dobra perde ao estreitar. Por isso há dois recuos, nesta ordem:

     900px  encolhe. Ainda cabe, mais apertada.
     640px  desaparece. Numa tela de 375px não existe posição em que a bolha
            não cubra texto — medido, e não há --cur-max que resolva.
            Desistir é a resposta certa: a nota é anotação, não conteúdo.

   Dobra que colapsa para uma coluna ANTES de 640px (o caso do
   hero/duas-colunas-produto, que vira coluna única em 980px) usa o
   .cur-layer-desktop abaixo. Quem quiser a nota no celular de qualquer
   forma sobrescreve display no tema — mas olhe no aparelho antes.
   ========================================================================== */
@media(max-width:980px){
  .cur-layer-desktop{display:none}
}

@media(max-width:900px){
  .cur-note{--cur-max:170px;--cur-bite:6px}
  .cur-bubble{font-size:11px;padding:7px 10px}
  .cur-tip{width:18px;height:21px}
}

@media(max-width:640px){
  .cur-layer{display:none}
}

script.js · 83 linhas · 3,0 KB

/* ==========================================================================
   LEGENDA DE CURSOR — o deslize entre âncoras

   Faz uma coisa só: lê as âncoras do data-cur-pos e escreve --cur-x/--cur-y
   na nota, em laço. Quem anima é o CSS, que declara a transição em left/top
   — nenhum `style.transform` sai daqui, e mudar a curva ou a duração é
   mexer no estilo.css, não neste arquivo.

   O formato é "x y; x y; x y", em qualquer unidade que o CSS aceite:
   porcentagem é o esperado (a âncora é relativa à camada), mas px e calc()
   passam intactos. Duas âncoras dão vai-e-volta; três ou mais viram ciclo.
   ========================================================================== */

(function () {
  var notas = document.querySelectorAll('[data-cur-note]');
  if (!notas.length) return;

  var HOLD = 2600;   /* parada padrão, em ms — medido na referência */

  /* Flag do core (contrato §7.5). O plano B existe para o componente rodar
     colado num Elementor onde o motion.js não entrou. */
  function reduzido() {
    if (window.LIB && typeof LIB.reduce === 'boolean') return LIB.reduce;
    return matchMedia('(prefers-reduced-motion: reduce)').matches &&
           location.search.indexOf('motion=on') < 0;
  }

  function ancoras(nota) {
    var bruto = nota.getAttribute('data-cur-pos') || '';
    var partes = bruto.split(';');
    var saida = [];
    for (var i = 0; i < partes.length; i++) {
      var par = partes[i].replace(/^\s+|\s+$/g, '').split(/\s+/);
      if (par.length === 2 && par[0] && par[1]) saida.push(par);
    }
    return saida;
  }

  function iniciar(nota) {
    var pontos = ancoras(nota);
    if (!pontos.length) return;

    var atual = 0;

    function pousar(n) {
      nota.style.setProperty('--cur-x', pontos[n][0]);
      nota.style.setProperty('--cur-y', pontos[n][1]);
    }

    /* A primeira âncora entra sempre, mesmo parado: é ela que tira a nota
       do canto superior esquerdo, que é onde o fallback do CSS a deixa. */
    pousar(0);

    /* Âncora única, ou movimento reduzido: acabou. Sem relógio ligado para
       não animar nada. */
    if (pontos.length < 2 || reduzido()) return;

    var espera = parseInt(nota.getAttribute('data-cur-hold'), 10) || HOLD;
    var relogio = null;

    function passo() {
      atual = (atual + 1) % pontos.length;
      pousar(atual);
    }

    function ligar()    { if (!relogio) relogio = setInterval(passo, espera); }
    function desligar() { if (relogio) { clearInterval(relogio); relogio = null; } }

    /* Fora da tela o relógio para. Sem isto a nota fica trocando de posição
       para sempre, muito depois de a dobra ter saído de vista — e são duas
       ou três por página. A referência não faz isso; nós fazemos. */
    if (window.IntersectionObserver) {
      var io = new IntersectionObserver(function (es) {
        if (es[0].isIntersecting) ligar(); else desligar();
      }, { threshold: 0 });
      io.observe(nota);
    } else {
      ligar();
    }
  }

  for (var n = 0; n < notas.length; n++) iniciar(notas[n]);
})();

Uma camada absoluta sobre a dobra, com bolhas de rótulo que carregam um ponteiro de mouse encostado num canto. Cada bolha desliza devagar entre duas ou três âncoras e para alguns segundos em cada uma. O olho lê como alguém apontando para a interface e dizendo o que ela faz.

Resolve um problema editorial específico: a dobra tem um argumento que não é a headline nem o subtítulo, e que perderia força numa lista de bullets. A legenda de cursor põe esse argumento no ar, ao lado do que ele descreve, sem consumir uma linha do bloco de texto. É um recurso de página de produto — em site institucional soa como aviso de software.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Bolha uma afirmação, sem ponto final, sem verbo no imperativo. Descreve a ferramenta, não manda usar
Extensão 40 a 60 caracteres. Duas linhas na bolha é o alvo; três já é parágrafo
Quantidade duas por dobra. Três é o teto absoluto, e só em dobra de tela cheia
Tom as duas notas nunca no mesmo roxo — uma leva .cur-note-deep

As notas de uma mesma dobra têm de ser paralelas em forma e diferentes em assunto: "Carteira desenhada para o seu horizonte" e "Relatório consolidado de todas as instituições" falam de coisas distintas no mesmo comprimento. Duas notas sobre o mesmo tema viram redundância exposta.

E o tempo de parada de cada nota é diferente de propósito (2600 e 3100 no markup). Em passo igual as duas viram um metrônomo, e o olho percebe o truque.

Especificação dos assets

Nenhum. O ponteiro é um <path> de 22×26 desenhado no próprio markup — não há imagem, ícone de biblioteca nem fonte a carregar.


Como usar

<link rel="stylesheet" href="componentes/efeitos/cursor-anotacao/estilo.css">
<script src="componentes/efeitos/cursor-anotacao/script.js"></script>

A camada é filha direta da seção. O core já dá position:relative a toda <section>, então não há nada a preparar:

<section class="sec-light sec-pad">
  <div class="cur-layer">
    <span class="cur-note cur-note-br cur-note-deep"
          data-cur-note data-cur-pos="5% 4%; 17% 4%" data-cur-hold="2600">
      <span class="cur-body" data-rv>
        <span class="cur-tip" aria-hidden="true"><svg viewBox="0 0 22 26">...</svg></span>
        <span class="cur-bubble">Relatório consolidado de todas as instituições</span>
      </span>
    </span>
  </div>
  <!-- a dobra -->
</section>

Ganchos

Gancho Papel
.cur-layer a camada. Absoluta, cobre a seção, pointer-events:none
[data-cur-note] a nota. O script escreve --cur-x/--cur-y aqui
data-cur-pos âncoras, "x y; x y", em porcentagem da camada. Uma = parada, duas = vai-e-volta, três+ = ciclo
data-cur-hold parada em ms. Padrão 2600
.cur-note-tl -tr -br -bl o canto onde o ponteiro encosta. O canto quadrado da bolha é a direção
--cur-skin cor da bolha e do ponteiro, os dois de uma vez
--cur-ink cor do texto
--cur-bite quanto o ponteiro entra na bolha. Padrão 8px
--cur-max largura máxima. Padrão 212px
--cur-radius raio da bolha. Padrão 10px
.cur-layer-desktop na camada: apaga tudo abaixo de 980px, e não de 640px

Variações


Gotchas


Histórico

Projeto Onde Data Diferenças
referência: Corvex https://usecorvex.com.br/ — hero 2026-08-31 referência visual, não origem de código

Normalizações aplicadas na tradução: a referência é Framer — classes hash, sem licença declarada, e nada foi copiado (§11). O que veio dela: a ideia da bolha com ponteiro encostado no canto quadrado, o porte (bolha ~187×48, ponteiro ~23×27), a cadência de ~2,5s de parada e o par de roxos. O que é nosso: o ponteiro desenhado do zero; a cor toda em --accent e color-mix em vez dos dois hex cravados; a entrada por data-rv do core (a referência usava opacidade em mola, sem blur — o §5 pede blur); as âncoras como contrato de dado (data-cur-pos) em vez de posições cravadas no CSS; os quatro cantos em vez dos dois que a referência tem; o desligamento fora da tela; e o aria-hidden só no ponteiro, deixando o texto da bolha legível para leitor de tela — na referência a bolha inteira é decoração.

Divergências conhecidas vs. a referência: o original desliza com mola do Motion, que passa ~1,5% do alvo e volta; aqui é o --ease único do projeto, sem overshoot. A diferença é imperceptível no percurso de ~150px, e um token de mola é decisão de arquitetura (§9.3) que não se justifica por isso. A referência também troca o texto das bolhas quando o carrossel da dobra vira de slide; aqui a nota é estática em conteúdo — quem quiser isso liga o hero/duas-colunas-produto às notas por conta própria.