Eine Textzeile mit gemischten Schriftgrößen kann die Werte für line-height beider Textspannen überschreiten, weil bei der Ausrichtung an der Grundlinie ihre oberen und unteren Ausdehnungen zusammenwirken. Diese Anleitung zeigt, wie du die zusätzliche Höhe diagnostizierst und eine Zeile mit zwei Beschriftungen in einem Mac-Desktopbrowser anpasst.

Warum bestimmt die höchste Textspanne nicht die Zeilenhöhe?

Betrachte eine abstrakte Seitenüberschrift mit einem großen Label A neben einem kleinen Label B. Beide Textspannen haben line-height: 40px, dennoch ist die umgebende Zeile höher als 40 CSS-Pixel. Das ergibt sich daraus, wie das Inline-Layout die Boxen ausrichtet, bevor es die umschließende Zeile berechnet.

Bei gewöhnlichem Inline-Text berechnet der Browser den Höhenbeitrag jeder Box, richtet die Boxen vertikal aus und umschließt dann ihre Gesamtausdehnungen. Bei Ausrichtung an der Grundlinie können die höchste Oberkante und die tiefste Unterkante zu verschiedenen Textspannen gehören. Das Ergebnis kann daher größer sein als jeder einzelne Beitrag. Auch das Elternelement trägt eine unsichtbare Box mit Mindestgröße bei, die traditionell Strut genannt wird. W3C: Berechnungen der Zeilenhöhe

Die Grundlinie dient als Ausrichtungsreferenz für den Text. Sie ist nicht der vertikale Mittelpunkt seines Zeilenhöhenbeitrags. Unterschiedlich großen Texten dieselbe Zeilenhöhe zu geben, bedeutet nicht, dass beide gleich weit oberhalb und unterhalb dieser Grundlinie liegen. Der initiale Wert von vertical-align ist baseline. MDN: vertical-align

Halte drei Messgrößen auseinander: die Metriken für Ober- und Unterlänge der Schrift, die sichtbaren Begrenzungen der jeweiligen Buchstaben und den Beitrag zum Zeilenlayout. Der farbige Hintergrund oder das Auswahlrechteck einer Textspanne ist kein direktes Maß für die umschließende Zeilenbox; CSS unterscheidet zwischen der Darstellungsgeometrie von Inline-Elementen und den Berechnungen der Zeilenhöhe. W3C: Durchschuss und halber Durchschuss

Wie können zwei Beiträge von 40 px eine Zeile von 48,4 px ergeben?

Für gewöhnlichen Text, der an der Grundlinie ausgerichtet ist und eine explizite Zeilenhöhe hat, lautet die hilfreiche Berechnung: Ziehe die Summe aus Ober- und Unterlänge der Schrift von der Zeilenhöhe ab und verteile den verbleibenden Raum gleichmäßig ober- und unterhalb. Dieser zusätzliche Raum heißt Durchschuss; jede Hälfte ist der halbe Durchschuss. Der CSS-Inline-Layout-Level-3-Arbeitsentwurf beschreibt diese Berechnung und lässt negativen Durchschuss zu. Sein Entwurfsstatus belegt keine Browserunterstützung für alle neueren darin enthaltenen Eigenschaften. W3C: Berechnung der Layoutgrenzen

Die folgenden Werte sind veranschaulichend und keine Messwerte für Arial oder eine Mac-Schrift. Angenommen, der Beitrag des Parent-Struts entspricht dem der kleinen Textspanne und es gibt keinen weiteren Inline-Inhalt oder vertikale Versätze.

Beitrag Oberlänge der Schrift Unterlänge der Schrift Zeilenhöhe Halber Durchschuss Oberhalb der Grundlinie Unterhalb der Grundlinie
Große Textspanne 32px 8px 40px 0px 32px 8px
Kleine Textspanne 9.6px 2.4px 40px 14px 23.6px 16.4px
Parent-Strut 9.6px 2.4px 40px 14px 23.6px 16.4px

In diesem Beispiel mit Ausrichtung an der Grundlinie wird die größte Ausdehnung jeder Seite separat herangezogen: max(32, 23.6, 23.6) + max(8, 16.4, 16.4) = 48.4px. Jeder Beitrag ist insgesamt 40px hoch, doch die große Textspanne bestimmt die Oberkante, während die kleine Textspanne und der Parent die Unterkante bestimmen. Dieses berechnete Ergebnis leitet sich aus der dokumentierten Layoutberechnung ab und ist keine vorhergesagte Browsermessung. W3C: Inline-Layoutmodell

Die diagnostische Frage lautet also: Welcher Beteiligte bestimmt welche Kante? Wenn du die Zeilenhöhe der kleinen Textspanne verringerst, verschwindet möglicherweise ihr unterer Beitrag, während ein identischer Beitrag des Parent-Struts bestehen bleibt.

Wie kannst du das auf einem Mac nachstellen und untersuchen?

Verwende für diese Anleitung Chrome auf deinem Mac. Öffne einen neuen Tab, rufe about:blank auf und drücke dann Command + Option + J, um die Console zu öffnen. Chrome dokumentiert diese Tastenkombination zum Ausführen von JavaScript. Chrome DevTools: Überblick

Führe das vollständige Programm unten in diesem leeren Tab aus. Es erstellt drei synthetische Zeilen mit denselben Beschriftungen, gibt deren Messwerte aus und verwendet eine Shadow-Root, um die Demonstrationsstile zusammenzufassen. Eine Paketinstallation oder externe Bibliothek ist nicht erforderlich. Bei erneuter Ausführung ersetzt es die vorherige Demonstration.

(async () => {
  document.getElementById('line-box-demo')?.remove();
  const host = document.createElement('div');
  host.id = 'line-box-demo';
  document.body.append(host);
  const root = host.attachShadow({ mode: 'open' });
  const style = document.createElement('style');
  style.textContent = `
    :host { display: block; padding: 24px; color: #222; background: #fff; }
    .label { margin: 20px 0 6px; font: 14px Arial, sans-serif; }
    .row { font: 12px/40px Arial, sans-serif; white-space: nowrap;
      margin: 0; padding: 0; border: 0; background: #e8eef4; }
    .large { font-size: 40px; line-height: 40px; }
    .small { font-size: 12px; line-height: 40px; }
    .trim-span .small, .trim-both .small { line-height: 12px; }
    .trim-both { font-size: 40px; }
  `;
  root.append(style);
  const cases = [
    ['', 'Shared 40px line-height'],
    ['trim-span', 'Smaller span only'],
    ['trim-both', 'Parent and smaller span']
  ];
  const rows = cases.map(([className, label]) => {
    const caption = document.createElement('p');
    caption.className = 'label';
    caption.textContent = label;
    const row = document.createElement('div');
    row.className = `row ${className}`;
    for (const [size, text] of [['large', 'Label A'], ['small', ' Label B']]) {
      const span = document.createElement('span');
      span.className = size;
      span.textContent = text;
      row.append(span);
    }
    root.append(caption, row);
    return { label, row };
  });
  await document.fonts.ready;
  console.table(rows.map(({ label, row }) => ({
    case: label,
    parentLineHeight: getComputedStyle(row).lineHeight,
    largeLineHeight: getComputedStyle(row.children[0]).lineHeight,
    smallLineHeight: getComputedStyle(row.children[1]).lineHeight,
    rowHeight: row.getBoundingClientRect().height
  })));
})();

Das Programm wartet auf document.fonts.ready, das aufgelöst wird, nachdem das Laden der Dokument-Schriftarten und die zugehörigen Layoutarbeiten abgeschlossen sind. Wenn du später eine andere Schrift einführst oder den Inhalt änderst, miss erneut, nachdem die Änderung wirksam geworden ist. MDN: FontFaceSet.ready

Betrachte die drei Zeilen als kontrollierten Vergleich. In der ersten haben beide Textspannen und der Parent eine Zeilenhöhe von 40px. In der zweiten wird nur die Zeilenhöhe der kleinen Textspanne auf 12px verringert. In der dritten wird außerdem die Schriftgröße des Parents auf 40px geändert, sodass sein Strut dieselbe Schriftgröße und Zeilenhöhe wie der große Text hat. Wenn die zweite Zeile hoch bleibt, die dritte aber schrumpft, zeigt dieser Vergleich den Beitrag des Parents.

Orientiere dich an den Ergebnissen deines Browsers, statt den veranschaulichenden Wert von 48,4px zu erwarten. Das Programm misst jede Zeile mit getBoundingClientRect(), das das Begrenzungsrechteck eines Elements einschließlich Innenabständen und Rahmen zurückgibt. Die Zeilen haben hier weder Innenabstände noch Rahmen, enthalten eine einzelne Zeile ohne Umbruch und haben eine automatische Höhe. Dadurch sind ihre Höhen brauchbare Näherungswerte für die Zeilenboxhöhen. Würde man eine Textspanne mit derselben API messen, erhielte man ihr Rechteck und nicht direkt die umschließende Zeilenbox. MDN: getBoundingClientRect()

Was solltest du in einem bestehenden Layout prüfen?

Klicke mit gedrückter Control-Taste auf den betreffenden Text und wähle Inspect. Untersuche unter Elements beide Textspannen und ihren umgebenden Block. Unter Styles kannst du Deklarationen nachverfolgen und unter Computed die angewendeten Werte sehen; aktiviere bei Bedarf Show All, um geerbte Eigenschaften zu untersuchen. Chrome dokumentiert diese Ansichten und ihre Eigenschaftsfilter. Chrome DevTools: Referenz zu CSS-Funktionen

Notiere für jeden Beteiligten font-family, font-size, line-height, display und vertical-align. Beginne mit dem Parent, statt nur die beiden Textspannen zu betrachten. Isoliere die Ursache anschließend mit diesen Schritten:

  1. Bestätige das Layoutmodell. Stelle fest, ob die untergeordneten Elemente am normalen Inline-Layout teilnehmen oder Flex- beziehungsweise Grid-Elemente sind.
  2. Bestätige eine einzelne Zeile. Verwende einen breiten Testbereich und kurze synthetische Beschriftungen, damit Umbrüche die Messung nicht verfälschen.
  3. Prüfe die Ausrichtung. Entferne vorübergehend Grundlinienverschiebungen oder nicht standardmäßige vertikale Ausrichtungen, bevor du die obige einfache Berechnung anwendest.
  4. Ändere einen Beitrag. Verringere die Zeilenhöhe der kleinen Textspanne, miss erneut und prüfe anschließend separat den Beitrag von Schrift und Zeilenhöhe des Parents.
  5. Stelle realistische Bedingungen wieder her. Prüfe nach dem Ermitteln der Ursache erneut die vorgesehene Schrift, die verfügbare Breite und den Text.

Prüfe auch die Vererbung. Ein einheitsloses line-height: 1.2 wird als Multiplikator vererbt, den jedes untergeordnete Element auf seine eigene Schriftgröße anwendet. Ein Längenwert wie 40px wird als feste Länge vererbt, sofern er nicht überschrieben wird; Prozentwerte werden anhand der Schriftgröße des deklarierenden Elements berechnet. Diese Werte können bei kleinem Text zu unerwartet großem Durchschuss führen. normal hängt von Schrift und Browser ab und eignet sich daher nicht als feste numerische Annahme. MDN: Werte und Vererbung von line-height

Welche Anpassung entspricht dem gewünschten Ergebnis?

Wähle die Anpassung entsprechend der Funktion der beiden Beschriftungen. Für eine kompakte Überschrift aus getrennten Beschriftungen gelten andere Anforderungen als für Wörter in einem umbrochenen Absatz.

Gewünschtes Ergebnis Zu testende Anpassung Zu überprüfen
Eine gemeinsame Textgrundlinie beibehalten Der kleinen Textspanne eine passende Zeilenhöhe geben und die Schriftmetriken des Parents an den Haupttext anpassen Der Parent-Strut liefert keine unerwünschte untere Ausdehnung mehr
Die Oberkanten getrennter Beschriftungen ausrichten vertical-align: top an beiden Inline-Textspannen testen Das veränderte Grundlinienverhältnis zum Design passt
Getrennte Beschriftungen innerhalb einer Zeile zentrieren Einen Flex-Container mit align-items: center verwenden Das Verhalten bei geringer Breite und die Umbrüche der Elemente weiterhin zum Layout passen
Den durchgehenden Absatzfluss beibehalten Inline-Text behalten und bewusst gewählte vererbte Zeilenhöhen verwenden Alle entstehenden Zeilen den gemischten Text aufnehmen

vertical-align: top richtet ein Element und seine Nachfahren an der Oberkante der Zeile aus. middle hat eine andere Definition: Es richtet die Mitte des Elements an der Grundlinie des Parents plus der Hälfte dessen x-Höhe aus. Es bedeutet daher keine geometrische Zentrierung innerhalb der gesamten Zeile, und eine Änderung der Ausrichtung garantiert keine bestimmte endgültige Höhe. MDN: Werte für vertical-align

Für zwei unabhängige Beschriftungen bietet Flexbox mit align-items eine explizite Ausrichtung quer zur Achse. Zentrierung und Ausrichtung an der Grundlinie drücken unterschiedliche Designentscheidungen aus. Der Wechsel zu Flexbox verändert den Layoutkontext. Verwende Flexbox also, wenn die Beschriftungen getrennte Elemente sind, und prüfe das Ergebnis bei geringerer Breite. MDN: align-items

Eine geringere Zeilenhöhe kann außerdem dazu führen, dass Text über seinen Layoutbeitrag hinausragt. Prüfe nach dem Verringern des Abstands die benachbarten Zeilen; eine kompakte gemessene Box allein belegt nicht, dass der Text ohne Überlappung hineinpasst. W3C: Durchschuss und halber Durchschuss

Können Canvas-Messungen das Browser-Zeilenlayout ersetzen?

Canvas-Messungen beantworten eine verwandte Frage. WHATWG definiert measureText() und unterscheidet die Schriftmetriken für Ober- und Unterlänge von den tatsächlichen Begrenzungswerten des jeweiligen Textes. Diese Messwerte sind für das Zeichnen auf dem Canvas hilfreich, doch eine Messung der Tintengrenzen berücksichtigt weder den Strut des umgebenden DOM-Parents noch die Inline-Ausrichtung oder die Beiträge zur Zeilenhöhe. Verwende sie nicht allein, um die Höhe dieser Zeile vorherzusagen. WHATWG: TextMetrics

Bewahre beim Experimentieren die Grenzen der Textgestaltung. CSS verlangt, dass die Textgestaltung über Inline-Grenzen hinweg fortgesetzt wird, wenn keine wirksame Formatierungsänderung vorliegt. Bestimmte Grenzen, darunter eine nicht anfängliche vertikale Ausrichtung, erfordern dagegen einen Abbruch der Textgestaltung. Um einzelne Zeichen zu messen, solltest du sie nicht einzeln umschließen: Das Aufteilen von Graphemclustern oder Ligaturen kann zu uneinheitlichen Ergebnissen führen. Lass vollständige Beschriftungen oder sinnvolle Textabschnitte intakt. W3C: Textverarbeitung und Textgestaltung über Elementgrenzen hinweg

Um die zusätzliche Höhe zu beheben, ermittle den Beteiligten, der die Oberkante bestimmt, und den, der die Unterkante bestimmt, einschließlich des Parents. Passe ihre Zeilenhöhenbeiträge an oder wähle das Ausrichtungsmodell, das dem Zweck der Beschriftungen entspricht. Miss anschließend die gesamte Zeile erneut mit dem vorgesehenen Text und der vorgesehenen Schrift.

Quellen