Benutzerdefinierte Textformatierungen

Verwenden Sie die nativen text-decoration-Eigenschaften, um Unterstreichungen, Überstreichungen und Durchstreichungen zu gestalten, anstatt sie mit Rahmen, Hintergrundverläufen oder Pseudoelementen zu fälschen. Eine native Dekoration folgt dem Text über Zeilenumbrüche hinweg, berücksichtigt die Messwerte der Schriftart, überspringt die Unterlängen von Glyphen und wird über verschachtelte Elemente vererbt. All das muss also nicht neu implementiert werden. Verwenden Sie die am Ende dieses Leitfadens beschriebenen boxbasierten Techniken nur, wenn die Dekoration den Text umranden oder umbrechen soll, anstatt entlang des Textes zu verlaufen.

Textdekorationen gelten für jeden Text, nicht nur für Links. Wenn unterstrichener Text kein Link ist, achten Sie darauf, dass er nicht mit einem Link verwechselt werden kann. Verwenden Sie beim Dekorieren eines Links ein echtes <a href>, das auf das Ziel verweist (fügen Sie target="_blank" rel="noopener" hinzu, um einen neuen Tab zu öffnen), nicht href="javascript:void(0)" mit einem Skript-Handler. Bei einem echten Link bleiben die Funktionen für den Klick mit der mittleren Maustaste, das Kopieren des Links und das Verhalten von Screenreadern erhalten.

Linie gestalten

Legen Sie die Linie, den Stil, die Farbe und die Stärke mit der Kurzschreibweise text-decoration und den Abstand zum Text mit text-underline-offset fest. Setzen Sie den Selektor in :where(), damit die Standardwerte keine Spezifität haben und jede spätere Überschreibung gewinnt, ohne dass benutzerdefinierte Eigenschaften erforderlich sind.

: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 akzeptiert solid, double, dotted, dashed und wavy. text-underline-offset gilt nur für Unterstreichungen.

Ausgangspunkte für das Design:

  • Stärke: etwa 0.06em bis 0.1em für den Textkörper. Verwenden Sie für Überschriften und hervorgehobene Zustände eine größere Stärke (0.1em bis 0.15em).
  • Offset: etwa 0.15em bis 0.25em. Gepunktete und gewellte Linien benötigen etwas mehr Platz als durchgezogene Linien. Erhöhen Sie daher den Versatz für diese Linien.
  • Farbe: Mit currentColor wird die Farbe der Linie an die des Texts angepasst. Eine weichere Mischung der Textfarbe, die beim Bewegen des Mauszeigers darüber intensiver wird, sorgt dafür, dass lange Linklisten nicht zu unruhig wirken.
  • Stil: solid für Links, dotted für Begriffe und Abkürzungen und wavy für Anmerkungen wie Fehler.

Festlegen, wo die Zeile übersprungen wird

Tintenaussetzer

Standardmäßig werden Unter- und Überstreichungen um Glyphenteile herum unterbrochen, die sie kreuzen, z. B. Unterlängen. Legen Sie text-decoration-skip-ink fest, um das Verhalten auszuwählen:

.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;
}
  • Behalten Sie auto für lateinischen Text bei und deklarieren Sie es nicht, da es die Standardeinstellung ist.
  • Verwenden Sie all, wenn die Unterstreichung so nah an der Grundlinie liegt, dass sie ideografische Glyphen kreuzt, oder wenn Text mit gemischten Skripten einheitlich aussehen muss.
  • Verwenden Sie none für eine sehr dicke oder eine dekorative Linie, bei der Lücken wie Unterbrechungen aussehen würden.

Gruppenbereiche überspringen

text-decoration-skip-spaces legt fest, ob die Linie unter Leerzeichen gezeichnet wird. Der Standardwert start end lässt führende und nachfolgende Leerzeichen unverändert.

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

Deklariere text-decoration-skip-spaces nur, wenn die Dekoration sichtbar den Abstand (none) abdecken oder zwischen jedem Wort (all) umbrechen soll. Gewöhnlicher Inline-Text, wie der Text eines Links, hat keine führenden oder nachfolgenden Leerzeichen, daher hat die Deklaration keine Auswirkungen.

Linie mit einem Einsatz kürzen oder verlängern

text-decoration-inset verschiebt den Anfang und das Ende der Linie. Ein positiver Wert zieht ein Ende nach innen, ein negativer Wert drückt es nach außen. Ein Wert verschiebt beide Enden, zwei Werte legen Start und Ende separat fest. Prozentwerte beziehen sich auf die Inline-Größe des dekorierten Felds.

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

Wenn Sie beim Hovern eine Unterstreichung ohne Rahmen oder Hintergrundverlauf anzeigen möchten, kürzen Sie das Ende um 100 % und setzen Sie es wieder auf 0 zurück:

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

Benutzerdefinierte Linie mit einem Pseudoelement zeichnen

Verwenden Sie ein Pseudoelement, wenn die Linie eine Form sein muss, die text-decoration nicht zeichnen kann, z. B. eine Wellenlinie, und machen Sie sie durch Clipping sichtbar. Es ist kein zusätzliches Markup erforderlich, sodass der Link ein einfaches <a> bleibt.

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

Text stattdessen mit einem Rahmen umranden

Zwei Box-Eigenschaften dekorieren Text, indem sie ihn umranden, anstatt eine Linie daran entlang zu ziehen. Fügen Sie dem Text einen Abstand hinzu, damit die Kontur ihn nicht berührt.

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

Mit border-shape wird nur der Rahmen ersetzt. Der Hintergrund ist immer noch das rechteckige Feld. Verlassen Sie sich also nicht darauf, dass er eine Form ausfüllt.

Wenn nur die Ecken eine andere Form haben sollen, z. B. ein abgeschrägtes, abgerundetes oder eingekerbtes Label, verwenden Sie stattdessen corner-shape mit border-radius. Hintergrund und Rahmen folgen den Ecken und nicht unterstützte Browser greifen auf normale abgerundete Ecken zurück.

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

Technik auswählen

  • text-decoration (Linie, Tintenüberspringen, Leerzeichen, Einzug): Die Linie sollte am Text entlang verlaufen, mit ihm umbrechen und seinen Schriftmetriken folgen. Dies ist die Standardeinstellung.
  • Pseudoelement mit border-shape und clip-path: Die Linie ist eine benutzerdefinierte Form oder erfordert eine Draw-on-Animation. Wählen Sie diese Option nur aus, wenn text-decoration-style das gewünschte Aussehen nicht erzeugen kann, weil eine zusätzliche Positionierung erforderlich ist.
  • background-clip: border-area: Der Text benötigt eine Kontur mit Farbverlauf. Verwenden Sie ein einfaches border-color, wenn ein durchgehender Umriss ausreicht.
  • corner-shape: Nur die Ecken müssen eine andere Form haben, z. B. abgeschrägt, abgerundet oder eingekerbt. Kombinieren Sie es mit border-radius, um die Eckengröße festzulegen.
  • border-shape: Für die Kontur ist eine Form erforderlich, die border-radius nicht zeichnen kann, z. B. ein Tag oder ein Pfeil. Verwenden Sie border-radius, wenn abgerundete Ecken ausreichen.
  • Inset Hover Reveal: Es lohnt sich, diese Option hinzuzufügen, wenn ein Link über eine Farbänderung hinaus eine Hover-Affordanz benötigt. Überspringen Sie ihn bei Text, der nicht interaktiv ist.

Fallback-Strategien

Bei jedem Fallback bleibt der Text lesbar und die wesentliche Dekoration erhalten. Formulieren Sie den Bedarf im Hinblick auf Ihr Baseline-Ziel: Wenn es Browser ohne eine bestimmte Eigenschaft enthält, wenden Sie den entsprechenden Fallback an.

Tintenaussetzer all

Wenn Ihr Baseline-Ziel text-decoration-skip-ink: all nicht unterstützt, wird die Deklaration ignoriert und der Browser behält auto bei. Lateinische Unterlängen werden weiterhin übersprungen. Nur bei Schriften, die nicht von auto abgedeckt werden, wie z. B. Japanisch, wird das Überspringen von Tinte nicht mehr angewendet. Das ist eine graduelle Fehlertoleranz, sodass kein zusätzlicher Code erforderlich ist.

Gruppenbereiche überspringen

Wenn Ihr Baseline-Ziel text-decoration-skip-spaces nicht unterstützt, verwendet der Browser seine eigene Standardbehandlung von Leerzeichen. Das ist graduelle Fehlertoleranz: Die Linie wird unter Leerzeichen gezogen, die der Browser nicht überspringt.

Eingebettet

Wenn Ihr Baseline-Ziel text-decoration-inset nicht unterstützt, wird die Deklaration ignoriert. Der Link behält dann seine normale, vollständig sichtbare Unterstreichung und der Übergang hat nichts zu animieren. Das ist eine graduelle Fehlertoleranz: Die Hover-Anzeige geht verloren und der Link bleibt unterstrichen.

Grenzgebiet

Wenn Ihr Baseline-Ziel background-clip: border-area nicht unterstützt, wird die Deklaration ignoriert und der Hintergrund wird unter dem gesamten Rahmenfeld gezeichnet, sodass der Text ausgefüllt wird, anstatt nur seine Kontur. Geben Sie sich NICHT mit einem einfachen einfarbigen Rahmen zufrieden, wenn der Farbverlauf Teil des Designs ist. Reproduzieren Sie stattdessen den Farbverlauf und wenden Sie background-clip: border-area nur innerhalb von @supports an.

Verwenden Sie eine opake Ebene, wenn der Hintergrund des Elements eine bekannte flache Farbe hat. Die Oberflächenfarbe wird über die Padding-Box und der Farbverlauf unter die Border-Box gelegt, sodass der Farbverlauf nur durch den transparenten Rahmen sichtbar ist:

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

Verwenden Sie ein maskiertes Pseudoelement, wenn das Innere transparent bleiben muss. Das Pseudoelement enthält den Farbverlauf und eine Maske schneidet alles außer dem Padding-Ring aus:

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

Rahmenform

Wenn Ihr Baseline-Ziel border-shape nicht unterstützt, wird die Deklaration ignoriert und das Element behält seinen rechteckigen Rahmen. Ein abgerundetes Rechteck ist nur dann ein akzeptabler Fallback, wenn die Form rein dekorativ ist. Wenn die Form eine Bedeutung hat, z. B. ein Tag, ein Pfeil oder ein Callout, reproduzieren Sie sie so genau wie möglich und wenden Sie border-shape nur innerhalb von @supports an.

Zeichnen Sie den Umriss mit einem ausgeschnittenen Pseudoelement. Behalten Sie einen transparenten Rahmen bei, damit das Feld dieselbe Größe wie im unterstützten Fall hat. Füllen Sie das Pseudoelement mit der Umrissfarbe und schneiden Sie es mit einem evenodd clip-path in einen Ring: die äußere Form, dann dieselbe Form, die um die Strichbreite versetzt ist.

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

Wenn Sie eine ausgefüllte Form anstelle eines Umrisses verwenden möchten, fügen Sie clip-path: polygon(...) mit denselben Punkten direkt in das Element ein.

Zeichnen Sie für die Unterstreichung des Pseudoelements eine gerade Linie mit border-block-end auf .draw::after und wenden Sie die Wellenlinie block-size, border und border-shape nur innerhalb von @supports (border-shape: circle(50%)) an. Die Linie ist gerade, wird aber trotzdem gezeichnet, da die clip-path-Animation nicht von der Form abhängt.

clip-path schneidet auch Fokusringe und box-shadow ab. Verlassen Sie sich nicht auf einen Umriss des abgeschnittenen Elements, um den Fokus anzuzeigen. Gestalten Sie :focus-visible stattdessen mit einer Farb- oder Hintergrundänderung.