Effetti testo personalizzati

Utilizza le proprietà native text-decoration per applicare lo stile a sottolineature, sopralinee e barrature, anziché simularle con bordi, gradienti di sfondo o pseudo-elementi. Una decorazione nativa segue il testo su righe con ritorno a capo, tiene traccia delle metriche del carattere, ignora i tratti discendenti dei glifi ed eredita tramite elementi nidificati, quindi non è necessario reimplementare nulla di tutto ciò. Utilizza le tecniche basate su box alla fine di questa guida solo quando la decorazione deve delineare o avvolgere il testo anziché scorrere lungo di esso.

Le decorazioni del testo si applicano a qualsiasi testo, non solo ai link. Se il testo sottolineato non è un link, assicurati che non possa essere scambiato per tale. Quando decori un link, utilizza un <a href> reale che rimandi alla destinazione (aggiungi target="_blank" rel="noopener" per aprire una nuova scheda), non href="javascript:void(0)" con un gestore di script. Un link reale mantiene il comportamento del clic con il tasto centrale del mouse, della copia del link e dello screen reader.

Definisci lo stile della linea

Imposta la linea, lo stile, il colore e lo spessore con la sintassi abbreviata text-decoration e lo spazio dal testo con text-underline-offset. Racchiudi il selettore in :where() in modo che i valori predefiniti abbiano una specificità pari a zero e qualsiasi override successivo abbia la precedenza, senza bisogno di proprietà personalizzate.

: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 accetta solid, double, dotted, dashed e wavy. text-underline-offset si applica solo alle sottolineature.

Punti di partenza per il design:

  • Spessore: circa 0.06em-0.1em per il corpo del testo. Usa uno spessore maggiore (da 0.1em a 0.15em) per le intestazioni e gli stati enfatizzati.
  • Offset: da circa 0.15em a 0.25em. Le linee tratteggiate e ondulate richiedono un po' più di spazio rispetto a quelle continue, quindi aumenta l'offset.
  • Colore: currentColor mantiene la linea abbinata al testo. Un mix più tenue del colore del testo che si intensifica al passaggio del mouse mantiene l'aspetto dei link più discreto.
  • Stile: solid per i link, dotted per i termini e le abbreviazioni e wavy per le annotazioni come gli errori.

Controlla dove viene saltata la riga

Inchiostro che salta

Per impostazione predefinita, le sottolineature e le sopralineature si interrompono intorno alle parti dei glifi che le attraversano, come i tratti discendenti. Imposta text-decoration-skip-ink per scegliere il 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;
}
  • Mantieni auto per il testo in caratteri latini e non dichiararlo: è l'impostazione predefinita.
  • Utilizza all quando la sottolineatura è abbastanza vicina alla linea di base da intersecare i glifi ideografici o quando il testo con script misti deve avere un aspetto coerente.
  • Utilizza none per una linea molto spessa o volutamente decorativa, in cui gli spazi vuoti sembrerebbero interruzioni.

Salta gli spazi

text-decoration-skip-spaces decide se la linea viene tracciata sotto gli spazi. Il valore predefinito, start end, lascia gli spazi iniziali e finali senza decorazioni.

.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;
}

Non dichiarare text-decoration-skip-spaces, a meno che la decorazione non debba coprire visibilmente la spaziatura interna (none) o interrompersi tra ogni parola (all). Il testo in linea ordinario, come il testo di un link, non ha spazi iniziali o finali, quindi la dichiarazione non ha alcun effetto.

Taglia o estendi la linea con un inserto

text-decoration-inset sposta l'inizio e la fine della linea. Un valore positivo sposta un'estremità verso l'interno, mentre un valore negativo la sposta verso l'esterno. Un valore sposta entrambe le estremità; due valori impostano l'inizio e la fine separatamente. Le percentuali si riferiscono alla dimensione inline della casella decorata.

/* 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); }

Per mostrare una sottolineatura al passaggio del mouse senza un bordo o un gradiente di sfondo, taglia la fine del 100% e riportala a 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;
  }
}

Disegna una linea personalizzata con uno pseudo-elemento

Utilizza uno pseudo-elemento quando la linea deve essere una forma che text-decoration non può disegnare, ad esempio uno scarabocchio, e mostrala tramite ritaglio. Non è necessario alcun markup aggiuntivo, quindi il link rimane un semplice <a>.

<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 */
  }
}

Contorna il testo con una casella

Due proprietà della casella decorano il testo delineandolo anziché tracciando una linea lungo il testo. Aggiungi spaziatura interna al testo in modo che il contorno non lo tocchi.

/* 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 sostituisce solo il bordo. Lo sfondo è ancora la casella rettangolare, quindi non fare affidamento su di esso per riempire una forma.

Quando solo gli angoli devono avere una forma diversa, ad esempio un'etichetta smussata, arrotondata o con intagli, utilizza corner-shape con border-radius. Mantiene lo sfondo e il bordo seguendo gli angoli, mentre i browser non supportati tornano ai normali angoli arrotondati.

.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 */
}

Scegli una tecnica

  • text-decoration (linea, interruzione dell'inchiostro, spazi, rientro): la linea deve scorrere lungo il testo, andare a capo con il testo e seguire le metriche dei caratteri. Questa è la scelta predefinita.
  • Pseudo-elemento con border-shape e clip-path: la linea è una forma personalizzata o richiede un'animazione di disegno. Sceglila solo quando text-decoration-style non può produrre l'aspetto, perché richiede un posizionamento aggiuntivo.
  • background-clip: border-area: il testo deve avere un contorno con gradiente. Utilizza un border-color semplice quando è sufficiente un contorno solido.
  • corner-shape: solo gli angoli devono avere una forma diversa, ad esempio smussata, arrotondata o dentellata. Abbinalo a border-radius, che imposta la dimensione dell'angolo.
  • border-shape: il contorno richiede una forma che border-radius non può disegnare, ad esempio un tag o una freccia. Utilizza border-radius quando gli angoli arrotondati sono sufficienti.
  • Inset hover reveal: vale la pena aggiungerlo quando un link richiede un affordance al passaggio del mouse oltre a un cambiamento di colore. Ignoralo per il testo non interattivo.

Strategie di fallback

Ogni opzione di riserva mantiene il testo leggibile e la decorazione principale. Inquadra la necessità in termini di target di Baseline: se include browser senza una proprietà, applica il rispettivo fallback.

Tratto discontinuo all

Se il tuo target Baseline non supporta text-decoration-skip-ink: all, la dichiarazione viene ignorata e il browser mantiene auto. I tratti discendenti latini vengono comunque saltati; solo gli script non coperti da auto, come il giapponese, perdono il salto dell'inchiostro. Si tratta di una riduzione controllata, quindi non è necessario alcun codice aggiuntivo.

Salta gli spazi

Se il tuo target Baseline non supporta text-decoration-skip-spaces, il browser utilizza la gestione predefinita degli spazi. Si tratta di una riduzione controllata: la linea viene tracciata sotto gli spazi che il browser non salta.

Inset

Se il target Baseline non supporta text-decoration-inset, la dichiarazione viene ignorata, quindi il link mantiene la sottolineatura normale e completamente visibile e la transizione non ha nulla da animare. Questa è la riduzione controllata: la visualizzazione al passaggio del mouse viene persa e il link rimane sottolineato.

Area di confine

Se il tuo target Baseline non supporta background-clip: border-area, la dichiarazione viene ignorata e lo sfondo viene applicato sotto l'intera casella del bordo, riempiendo il testo anziché solo il suo contorno. NON accontentarti di un bordo a tinta unita se il gradiente fa parte del design. Riproduci invece il gradiente e applica background-clip: border-area solo all'interno di @supports.

Utilizza un livello opaco quando lo sfondo dell'elemento è un colore piatto noto. Dipingi il colore della superficie sopra la casella di spaziatura interna e il gradiente sotto la casella del bordo, in modo che il gradiente sia visibile solo attraverso il bordo trasparente:

.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 */
  }
}

Utilizza uno pseudo-elemento mascherato quando l'interno deve rimanere trasparente. Lo pseudo-elemento contiene il gradiente e una maschera ritaglia tutto tranne l'anello di spaziatura interna:

@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;
  }
}

Forma del bordo

Se il tuo target Baseline non supporta border-shape, la dichiarazione viene ignorata e l'elemento mantiene il bordo rettangolare. Un rettangolo arrotondato è un'alternativa accettabile solo quando la forma è puramente decorativa. Quando la forma ha un significato, ad esempio un tag, una freccia o un callout, riproducila nel modo più fedele possibile e applica border-shape solo all'interno di @supports.

Disegna il contorno con uno pseudo-elemento ritagliato. Mantieni un bordo trasparente in modo che la casella abbia le stesse dimensioni del caso supportato, riempi lo pseudo-elemento con il colore del contorno e taglialo in un anello con un evenodd clip-path: la forma esterna, quindi la stessa forma inserita dalla larghezza del tratto.

.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%);
  }
}

Per una forma piena anziché un contorno, inserisci clip-path: polygon(...) con gli stessi punti direttamente sull'elemento.

Per la sottolineatura dello pseudo-elemento, disegna una linea retta con border-block-end su .draw::after e applica la linea ondulata block-size, border e border-shape solo all'interno di @supports (border-shape: circle(50%)). La linea è dritta, ma viene comunque disegnata perché l'animazione clip-path non dipende dalla forma.

clip-path ritaglia anche gli anelli di messa a fuoco e box-shadow. Non fare affidamento su un contorno dell'elemento ritagliato per mostrare lo stato attivo; applica invece uno stile a :focus-visible con un cambiamento di colore o di sfondo.