Estilos de texto personalizados

Use as propriedades nativas de text-decoration para estilizar sublinhados, sobrelinhas e tachados, em vez de simular esses efeitos com bordas, gradientes de plano de fundo ou pseudoelementos. Uma decoração nativa segue o texto em linhas quebradas, rastreia as métricas da fonte, ignora os descendentes de glifos e herda elementos aninhados. Portanto, nada disso precisa ser reimplementado. Use as técnicas baseadas em caixas no final deste guia apenas quando a decoração precisar contornar ou envolver o texto, em vez de seguir ao longo dele.

As decorações de texto se aplicam a qualquer texto, não apenas a links. Se o texto sublinhado não for um link, verifique se ele não pode ser confundido com um. Ao decorar um link, use um <a href> real que aponte para o destino (adicione target="_blank" rel="noopener" para abrir uma nova guia), não href="javascript:void(0)" com um gerenciador de script. Um link real mantém o comportamento de clique com o botão do meio, cópia de link e leitor de tela.

Estilizar a linha

Defina a linha, o estilo, a cor e a espessura com a abreviação text-decoration e o espaçamento do texto com text-underline-offset. Envolva o seletor em :where() para que os padrões tenham especificidade zero e qualquer substituição posterior vença, sem precisar de propriedades personalizadas.

:where(.decorated) {
  /* Thickness is relative to the font size (0.08em is an example value), so the line is not too heavy
     on small text or too light on large text, and max() keeps it at least 1px so it never disappears. */
  text-decoration: underline solid currentColor max(1px, 0.08em);
  /* Use em so the gap scales with the font size. 0.2em clears most descenders; example value. */
  text-underline-offset: 0.2em;
}

/* Example hover state: a softer line that firms up on hover and focus. The 50% mix and
   0.12em thickness are example values. */
:where(.decorated) {
  text-decoration-color: color-mix(in srgb, currentColor, transparent);
}

:where(.decorated:is(:hover, :focus-visible)) {
  text-decoration-color: currentColor;
  text-decoration-thickness: max(1px, 0.12em);
}

/* Other lines are a different text-decoration-line on the same properties */
.decorated-over    { text-decoration-line: overline; }
.decorated-through { text-decoration-line: line-through; }

text-decoration-style aceita solid, double, dotted, dashed e wavy. text-underline-offset se aplica apenas a sublinhados.

Pontos de partida para o design:

  • Espessura: cerca de 0.06em a 0.1em para o corpo do texto. Use uma espessura maior (0.1em a 0.15em) para títulos e estados enfatizados.
  • Deslocamento: cerca de 0.15em a 0.25em. As linhas pontilhadas e onduladas precisam de um pouco mais de espaço do que as sólidas. Por isso, aumente o deslocamento delas.
  • Cor: currentColor mantém a linha correspondente ao texto. Uma mistura mais suave da cor do texto que se destaca ao passar o cursor mantém as sequências longas de links mais discretas.
  • Estilo: solid para links, dotted para termos e abreviações e wavy para anotações, como erros.

Controlar onde a linha é pulada

Falha de tinta

Por padrão, os sublinhados e as linhas superiores são interrompidos em partes de glifos que os cruzam, como as hastes inferiores. Defina text-decoration-skip-ink para escolher o comportamento:

.decorated {
  /* auto: skip ink for most scripts (default). none: draw straight through.
     all: also skip ink in scripts where auto does not, such as Japanese or Chinese. */
  text-decoration-skip-ink: all;
}
  • Mantenha auto para texto em latim e não o declare: ele é o padrão.
  • Use all quando o sublinhado estiver próximo o suficiente da linha de base para cruzar glifos ideográficos ou quando o texto de script misto precisar parecer consistente.
  • Use none para uma linha muito grossa ou decorativa, em que as lacunas parecem quebras.

Pular espaços

text-decoration-skip-spaces decide se a linha é desenhada sob espaços. O padrão, start end, deixa os espaços à esquerda e à direita sem decoração.

.decorated {
  /* Spaces collapse by default, so there is nothing to skip. Preserve them when the
     leading or trailing spaces are meaningful, such as padded input values. */
  white-space: pre;
  /* none: decorate every space. all: skip every space, so each word is underlined separately.
     start | end | start end: skip only leading, trailing, or both. */
  text-decoration-skip-spaces: all;
}

Não declare text-decoration-skip-spaces, a menos que a decoração cubra visivelmente o padding (none) ou quebre entre cada palavra (all). O texto inline comum, como o texto de um link, não tem espaços à esquerda ou à direita, então a declaração não faz nada.

Cortar ou estender a linha com uma inserção

text-decoration-inset move o início e o fim da linha. Um valor positivo puxa uma extremidade para dentro, e um valor negativo a empurra para fora. Um valor move as duas extremidades, enquanto dois valores definem o início e o fim separadamente. As porcentagens se referem ao tamanho inline da caixa decorada.

/* Small insets soften the ends of the line; larger ones read as a deliberately short line.
   Both values are examples. */
.trimmed  { text-decoration-inset: var(--deco-inset, 0.1em); }
.extended { text-decoration-inset: calc(var(--deco-inset, 0.1em) * -1); }

Para revelar um sublinhado ao passar o cursor sem uma borda ou um gradiente de plano de fundo, corte o final em 100% e faça a transição de volta para 0:

.reveal {
  text-decoration: underline; /* Always visible, including for reduced motion */
}

/* Only hide and animate the line when the user has not asked for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .reveal {
    /* Trim the end by the full inline size so the line starts with zero length. */
    text-decoration-inset: 0 100%;
    /* 150ms to 300ms feels responsive without lagging; ease-out starts quickly. Example values. */
    transition: text-decoration-inset var(--reveal-duration, 0.25s) ease-out;
  }

  /* Include :focus-visible so keyboard users get the same reveal as pointer users. */
  .reveal:is(:hover, :focus-visible) {
    text-decoration-inset: 0 0;
  }
}

Desenhar uma linha personalizada com um pseudoelemento

Use um pseudoelemento quando a linha precisar ser uma forma que o text-decoration não pode desenhar, como um rabisco, e revele-a por recorte. Nenhuma marcação extra é necessária, então o link permanece como um <a> simples.

<a class="draw" href="#">Hover to draw underline</a>
.draw {
  position: relative;
  display: inline-block;
  color: inherit;
  text-decoration: none;
  padding-block-end: 0.4em; /* Space for the line below the text; example value */
}

.draw::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  block-size: 0.4em; /* Height of the squiggle; example value */
  pointer-events: none;
  border: var(--deco-thickness, max(1px, 0.08em)) solid var(--deco-color, currentColor);
  /* A single shape strokes along its path, using the border width as the stroke. Example squiggle. */
  border-shape: shape(from 0% 50%,
    curve to 20% 50% with 5% 0% / 15% 100%,
    smooth to 40% 50% with 35% 100%,
    smooth to 60% 50% with 55% 100%,
    smooth to 80% 50% with 75% 100%,
    smooth to 100% 50% with 95% 100%);
}

/* Only hide and animate the line when the user has not asked for reduced motion;
   otherwise it stays fully drawn. */
@media (prefers-reduced-motion: no-preference) {
  .draw::after {
    /* Clip the line away from the right edge. The negative block insets keep the stroke
       from being cut off vertically. */
    clip-path: inset(-1em 100% -1em 0);
    /* A slightly slower draw than a hover reveal reads as drawing; example duration. */
    transition: clip-path var(--draw-duration, 0.6s) ease-out;
  }

  .draw:is(:hover, :focus-visible)::after {
    clip-path: inset(-1em 0 -1em 0); /* Fully drawn */
  }
}

Contornar o texto com uma caixa

Duas propriedades de caixa decoram o texto contornando-o em vez de passar uma linha ao longo dele. Adicione padding ao texto para que o contorno não o toque.

/* Gradient outline with a see-through middle */
.ring {
  padding: 0.2em 0.75em; /* Example values: enough room that the outline does not touch the text */
  /* The border width is the visible outline thickness. Relative to the font size, with a 2px
     minimum so it stays visible on small text; example value. */
  border: var(--ring-width, max(2px, 0.12em)) solid transparent;
  background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
  /* Paint the background only under the border, so the inside stays transparent. */
  background-clip: border-area;
}

/* Border that follows a shape that border-radius cannot draw, such as a tag with a pointed end */
.tag {
  padding: 0.2em 1.5em 0.2em 0.9em; /* Example values: extra room at the point so the text is not clipped */
  border: var(--shape-stroke, max(2px, 0.08em)) solid var(--shape-color, currentColor); /* Example stroke width */
  /* A single shape strokes along its path. Two shapes fill the area between them.
     This polygon is an example; use shape() for curves. */
  border-shape: polygon(0 0, 85% 0, 100% 50%, 85% 100%, 0 100%);
}

border-shape substitui apenas a borda. O plano de fundo ainda é a caixa retangular, então não use-o para preencher uma forma.

Quando apenas os cantos precisam de uma forma diferente, como um rótulo chanfrado, arredondado ou entalhado, use corner-shape com border-radius. Ele mantém o plano de fundo e a borda seguindo os cantos, e os navegadores incompatíveis voltam aos cantos arredondados comuns.

.label {
  padding: 0.2em 0.9em; /* Example values */
  border: var(--shape-stroke, max(2px, 0.08em)) solid var(--shape-color, currentColor);
  border-radius: 0.6em; /* Corner size; example value */
  corner-shape: bevel; /* round (default) | scoop | bevel | notch | square | squircle */
}

Escolha uma técnica

  • text-decoration (linha, salto de tinta, espaços, inserção): a linha precisa acompanhar o texto, se ajustar a ele e seguir as métricas de fonte. Essa é a opção padrão.
  • Pseudoelemento com border-shape e clip-path: a linha é uma forma personalizada ou precisa de uma animação de desenho. Escolha essa opção apenas quando text-decoration-style não puder produzir o visual, porque ela precisa de posicionamento extra.
  • background-clip: border-area: o texto precisa de um contorno de gradiente. Use o border-color simples quando um contorno sólido for suficiente.
  • corner-shape: apenas os cantos precisam de uma forma diferente, como chanfrada, arredondada ou entalhada. Combine-o com border-radius, que define o tamanho do canto.
  • border-shape: o contorno precisa de uma forma que o border-radius não pode desenhar, como uma tag ou seta. Use border-radius quando cantos arredondados forem suficientes.
  • Revelação ao passar o cursor: vale a pena adicionar quando um link precisa de uma indicação de passagem do cursor além de uma mudança de cor. Pule essa etapa para textos que não são interativos.

Estratégias de fallback

Cada substituto mantém o texto legível e a decoração principal. Enquadre a necessidade em termos da meta do Baseline: se ela incluir navegadores sem uma propriedade, aplique o substituto correspondente.

Tinta pulando all

Se o destino do Baseline não for compatível com text-decoration-skip-ink: all, a declaração será ignorada e o navegador vai manter auto. As letras latinas com descendentes ainda são ignoradas. Somente as escritas que não são cobertas por auto, como o japonês, perdem a interrupção de tinta. Isso é uma degradação suave, então nenhum código extra é necessário.

Pular espaços

Se o destino do Baseline não for compatível com text-decoration-skip-spaces, o navegador vai usar o próprio processamento padrão de espaços. Isso é uma degradação suave: a linha é desenhada sob espaços que o navegador não pula.

Recuo

Se o destino do Baseline não for compatível com text-decoration-inset, a declaração será ignorada, então o link vai manter o sublinhado normal e totalmente visível, e a transição não terá nada para animar. Isso é degradação gradual: a revelação ao passar o cursor é perdida, e o link permanece sublinhado.

Área de fronteira

Se o Baseline de destino não for compatível com background-clip: border-area, a declaração será ignorada e o plano de fundo será pintado em toda a caixa de borda, preenchendo o texto em vez de apenas o contorno. NÃO use uma borda sólida simples quando o gradiente fizer parte do design. Em vez disso, reproduza o gradiente e aplique background-clip: border-area apenas dentro de @supports.

Use uma camada opaca quando o plano de fundo do elemento for uma cor simples conhecida. Pinte a cor da superfície sobre a caixa de padding e o gradiente abaixo da caixa de borda, para que o gradiente apareça apenas pela borda transparente:

.ring {
  border: var(--ring-width, max(2px, 0.12em)) solid transparent; /* Example width */
  /* Fallback: an opaque surface layer hides the gradient inside the border. */
  background:
    linear-gradient(var(--surface, canvas) 0 100%) padding-box,
    linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
}

@supports (background-clip: border-area) {
  .ring {
    background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
    background-clip: border-area; /* The inside stays transparent */
  }
}

Use um pseudoelemento mascarado quando o interior precisar permanecer transparente. O pseudoelemento carrega o gradiente, e uma máscara corta tudo, exceto o anel de padding:

@supports not (background-clip: border-area) {
  .ring {
    position: relative;
    border-color: transparent;
  }

  .ring::before {
    content: "";
    position: absolute;
    inset: calc(var(--ring-width, max(2px, 0.12em)) * -1); /* Cover the border box */
    padding: var(--ring-width, max(2px, 0.12em)); /* Ring thickness */
    border-radius: inherit;
    background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777));
    /* Keep only the area between the content box and the border box. */
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
    pointer-events: none;
  }
}

Formato da borda

Se o destino do Baseline não for compatível com border-shape, a declaração será ignorada e o elemento vai manter a borda retangular. Um retângulo arredondado só é aceitável como substituto quando a forma é puramente decorativa. Quando a forma tiver um significado, como uma tag, seta ou balão, reproduza-a da maneira mais fiel possível e aplique border-shape apenas dentro de @supports.

Desenhe o contorno com um pseudoelemento recortado. Mantenha uma borda transparente para que a caixa tenha o mesmo tamanho do caso compatível, preencha o pseudoelemento com a cor do contorno e corte-o em um anel com um evenodd clip-path: a forma externa e, em seguida, a mesma forma inserida pela largura do traço.

.tag {
  --stroke: max(2px, 0.08em); /* Example stroke width */
  position: relative;
  padding: 0.2em 1.5em 0.2em 0.9em; /* Example values */
  border: var(--stroke) solid transparent;
}

@supports not (border-shape: circle(50%)) {
  .tag::before {
    content: "";
    position: absolute;
    inset: calc(var(--stroke) * -1); /* Cover the border box */
    background: var(--shape-color, currentColor);
    pointer-events: none;
    /* Outer shape, then the inner shape inset by the stroke. The inner points are approximate on
       diagonal edges, so the stroke looks slightly thinner there. */
    clip-path: polygon(evenodd,
      0 0, 85% 0, 100% 50%, 85% 100%, 0 100%,
      var(--stroke) var(--stroke),
      calc(85% - var(--stroke)) var(--stroke),
      calc(100% - var(--stroke) * 2) 50%,
      calc(85% - var(--stroke)) calc(100% - var(--stroke)),
      var(--stroke) calc(100% - var(--stroke)));
  }
}

@supports (border-shape: circle(50%)) {
  .tag {
    border-color: var(--shape-color, currentColor);
    border-shape: polygon(0 0, 85% 0, 100% 50%, 85% 100%, 0 100%);
  }
}

Para uma forma preenchida em vez de um contorno, coloque clip-path: polygon(...) com os mesmos pontos diretamente no elemento.

Para o sublinhado do pseudoelemento, desenhe uma linha reta com border-block-end em .draw::after e aplique o ondulado block-size, border e border-shape somente dentro de @supports (border-shape: circle(50%)). A linha é reta, mas ainda é desenhada, porque a animação clip-path não depende da forma.

clip-path também corta círculos de foco e box-shadow. Não use um contorno no elemento cortado para mostrar o foco. Em vez disso, estilize :focus-visible com uma mudança de cor ou de plano de fundo.