Décorations de texte personnalisées

Utilisez les propriétés text-decoration natives pour appliquer un style aux soulignements, aux surlignements et aux barrés, au lieu de les simuler avec des bordures, des dégradés d'arrière-plan ou des pseudo-éléments. Une décoration native suit le texte sur les lignes renvoyées, suit les métriques de la police, ignore les jambages inférieurs des glyphes et hérite des éléments imbriqués. Il n'est donc pas nécessaire de réimplémenter ces éléments. N'utilisez les techniques basées sur les boîtes présentées à la fin de ce guide que lorsque la décoration doit entourer ou encapsuler le texte plutôt que de le suivre.

Les décorations de texte s'appliquent à n'importe quel texte, pas seulement aux liens. Si le texte souligné n'est pas un lien, assurez-vous qu'il ne peut pas être confondu avec un lien. Lorsque vous décorez un lien, utilisez un véritable <a href> pointant vers la destination (ajoutez target="_blank" rel="noopener" pour ouvrir un nouvel onglet), et non href="javascript:void(0)" avec un gestionnaire de script. Un vrai lien conserve le comportement du clic du milieu, de la copie du lien et du lecteur d'écran.

Appliquer un style à la ligne

Définissez la ligne, le style, la couleur et l'épaisseur avec le raccourci text-decoration, et l'écart par rapport au texte avec text-underline-offset. Encapsulez le sélecteur dans :where() pour que les valeurs par défaut n'aient aucune spécificité et que tout remplacement ultérieur l'emporte, sans avoir besoin de propriétés personnalisées.

: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 accepte solid, double, dotted, dashed et wavy. text-underline-offset ne s'applique qu'aux soulignements.

Points de départ de la conception :

  • Épaisseur : environ 0.06em à 0.1em pour le corps du texte. Utilisez une épaisseur plus importante (0.1em à 0.15em) pour les titres et les états mis en avant.
  • Décalage : environ 0.15em à 0.25em. Les lignes en pointillés et ondulées ont besoin d'un peu plus d'espace que les lignes pleines. Augmentez donc le décalage pour ces lignes.
  • Couleur : currentColor permet de faire correspondre la ligne au texte. Un mélange plus doux de la couleur du texte qui s'affirme au survol permet de ne pas surcharger les longues listes de liens.
  • Style : solid pour les liens, dotted pour les termes et les abréviations, et wavy pour les annotations telles que les erreurs.

Contrôler l'endroit où la ligne est ignorée

Sauts d'encre

Par défaut, les soulignements et les surlignements sont interrompus autour des parties de glyphes qui les croisent, comme les jambages inférieurs. Définissez text-decoration-skip-ink pour choisir le comportement :

.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;
}
  • Conservez auto pour le texte en alphabet latin et ne le déclarez pas, car il s'agit de la valeur par défaut.
  • Utilisez all lorsque le soulignement est suffisamment proche de la ligne de base pour croiser des glyphes idéographiques, ou lorsque le texte en écriture mixte doit être cohérent.
  • Utilisez none pour une ligne très épaisse ou délibérément décorative, où les espaces ressembleraient à des ruptures.

Ignorer les espaces

text-decoration-skip-spaces détermine si la ligne est tracée sous les espaces. La valeur par défaut, start end, laisse les espaces de début et de fin sans décoration.

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

Ne déclarez pas text-decoration-skip-spaces, sauf si la décoration doit couvrir visiblement la marge intérieure (none) ou s'interrompre entre chaque mot (all). Le texte intégré ordinaire, tel que le texte d'un lien, ne comporte pas d'espaces de début ni de fin. La déclaration n'a donc aucun effet.

Raccourcir ou prolonger la ligne avec un encart

text-decoration-inset déplace le début et la fin de la ligne. Une valeur positive tire une extrémité vers l'intérieur, tandis qu'une valeur négative la pousse vers l'extérieur. Une valeur déplace les deux extrémités, tandis que deux valeurs définissent le début et la fin séparément. Les pourcentages font référence à la taille intégrée de la zone décorée.

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

Pour afficher un soulignement au survol sans bordure ni dégradé d'arrière-plan, réduisez la fin de 100 % et revenez à 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;
  }
}

Tracer une ligne personnalisée avec un pseudo-élément

Utilisez un pseudo-élément lorsque la ligne doit être une forme que text-decoration ne peut pas dessiner, comme un gribouillage, et révélez-la en la découpant. Aucun balisage supplémentaire n'est nécessaire. Le lien reste donc un simple <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 */
  }
}

Encadrer le texte à la place

Deux propriétés de boîte décorent le texte en l'entourant plutôt qu'en traçant une ligne le long de celui-ci. Ajoutez une marge intérieure au texte pour que le contour ne le touche pas.

/* 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 remplace uniquement la bordure. L'arrière-plan reste la zone rectangulaire. Ne vous fiez donc pas à lui pour remplir une forme.

Lorsque seuls les angles doivent avoir une forme différente, comme un libellé biseauté, arrondi ou cranté, utilisez plutôt corner-shape avec border-radius. L'arrière-plan et la bordure suivent les angles, et les navigateurs non compatibles reviennent aux angles arrondis ordinaires.

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

Choisir une technique

  • text-decoration (ligne, saut d'encre, espaces, encart) : la ligne doit suivre le texte, revenir à la ligne avec lui et suivre ses métriques de police. Il s'agit du choix par défaut.
  • Pseudo-élément avec border-shape et clip-path : la ligne est une forme personnalisée ou nécessite une animation de dessin. Ne le choisissez que lorsque text-decoration-style ne peut pas produire l'apparence souhaitée, car il nécessite un positionnement supplémentaire.
  • background-clip: border-area : le texte nécessite un contour en dégradé. Utilisez un border-color simple lorsqu'un contour plein suffit.
  • corner-shape : seuls les angles doivent avoir une forme différente, par exemple biseautée, creusée ou crantée. Associez-le à border-radius, qui définit la taille des angles.
  • border-shape : le contour nécessite une forme que border-radius ne peut pas dessiner, comme un tag ou une flèche. Utilisez border-radius lorsque des angles arrondis suffisent.
  • Affichage au survol : utile lorsqu'un lien nécessite une affordance au survol au-delà d'un changement de couleur. Ignorez-le pour le texte non interactif.

Stratégies de remplacement

Chaque solution de remplacement permet de conserver la lisibilité du texte et la décoration principale. Formulez le besoin en fonction de votre cible Baseline : si elle inclut des navigateurs sans propriété, appliquez le remplacement correspondant.

Saut d'encre all

Si votre cible Baseline n'est pas compatible avec text-decoration-skip-ink: all, la déclaration est ignorée et le navigateur conserve auto. Les jambages inférieurs latins sont toujours ignorés. Seuls les scripts non couverts par auto, comme le japonais, perdent l'omission d'encre. Il s'agit d'une dégradation élégante, donc aucun code supplémentaire n'est nécessaire.

Ignorer les espaces

Si votre cible Baseline n'est pas compatible avec text-decoration-skip-spaces, le navigateur utilise sa propre gestion par défaut des espaces. Il s'agit d'une dégradation élégante : la ligne est tracée sous les espaces que le navigateur n'ignore pas.

Encart

Si votre cible de référence n'est pas compatible avec text-decoration-inset, la déclaration est ignorée. Le lien conserve donc son soulignement normal, entièrement visible, et la transition n'a rien à animer. Il s'agit d'une dégradation élégante : l'affichage au survol est perdu et le lien reste souligné.

Zone frontalière

Si votre cible de référence n'est pas compatible avec background-clip: border-area, la déclaration est ignorée et l'arrière-plan est peint sous l'ensemble de la zone de bordure, remplissant le texte au lieu de seulement son contour. Ne vous contentez PAS d'une bordure unie lorsque le dégradé fait partie du design. Reproduisez plutôt le dégradé et appliquez background-clip: border-area uniquement dans @supports.

Utilisez un calque opaque lorsque l'arrière-plan de l'élément est une couleur unie connue. Peignez la couleur de la surface sur la zone de marge intérieure et le dégradé sous la zone de bordure, de sorte que le dégradé ne s'affiche qu'à travers la bordure 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 */
  }
}

Utilisez un pseudo-élément masqué lorsque l'intérieur doit rester transparent. Le pseudo-élément porte le dégradé, et un masque découpe tout sauf son anneau de marge intérieure :

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

Forme de la bordure

Si votre cible de référence n'est pas compatible avec border-shape, la déclaration est ignorée et l'élément conserve sa bordure rectangulaire. Un rectangle arrondi n'est une solution de remplacement acceptable que lorsque la forme est purement décorative. Lorsque la forme a une signification, comme un tag, une flèche ou une légende, reproduisez-la aussi fidèlement que possible et appliquez border-shape uniquement à l'intérieur de @supports.

Dessinez le contour avec un pseudo-élément découpé. Conservez une bordure transparente pour que la zone ait la même taille que dans le cas compatible, remplissez le pseudo-élément avec la couleur du contour et découpez-le en anneau avec un evenodd clip-path : la forme extérieure, puis la même forme insérée par la largeur du trait.

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

Pour une forme pleine plutôt qu'un contour, placez clip-path: polygon(...) avec les mêmes points directement sur l'élément.

Pour le pseudo-élément de soulignement, tracez une ligne droite avec border-block-end sur .draw::after, puis appliquez l'ondulation block-size, border et border-shape uniquement dans @supports (border-shape: circle(50%)). La ligne est droite, mais elle continue de se dessiner, car l'animation clip-path ne dépend pas de la forme.

clip-path recadre également les anneaux de focus et box-shadow. Ne vous fiez pas à un contour sur l'élément tronqué pour indiquer la sélection. À la place, appliquez un style à :focus-visible en modifiant la couleur ou l'arrière-plan.