Eine Linie durch Text in CSS weben

Avatar of Ana Tudor
Ana Tudor am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Anfang des Jahres stieß ich auf diese Demo von Florin Pop, bei der eine Linie über oder unter den Buchstaben einer einzeiligen Überschrift verläuft. Ich fand die Idee gut, aber es gab ein paar kleine Dinge an der Implementierung, die ich meiner Meinung nach gleichzeitig vereinfachen und verbessern könnte.

Zunächst wird der Überschriftentext in der Original-Demo dupliziert, was ich wusste, dass man leicht vermeiden könnte. Dann ist da noch die Tatsache, dass die Länge der durch den Text verlaufenden Linie eine magische Zahl ist, was kein sehr flexibler Ansatz ist. Und schließlich, können wir das JavaScript loswerden?

Schauen wir uns also an, wohin ich das gebracht habe.

HTML-Struktur

Florin packt den Text in ein Überschriftenelement und dupliziert dann diese Überschrift. Mit Splitting.js wird der Textinhalt der duplizierten Überschrift durch Spans ersetzt, die jeweils einen Buchstaben des Originaltextes enthalten.

Da ich bereits beschlossen hatte, dies ohne Textduplizierung zu tun, erscheint die Verwendung einer Bibliothek zum Aufteilen des Textes in Zeichen und anschließendes Einfügen jedes Zeichens in ein span etwas übertrieben. Daher machen wir alles mit einem HTML-Präprozessor.

- let text = 'We Love to Play';
- let arr = text.split('');

h1(role='image' aria-label=text)
  - arr.forEach(letter => {
    span.letter #{letter}
  - });

Da das Aufteilen von Text in mehrere Elemente für Screenreader möglicherweise nicht gut funktioniert, haben wir dem Ganzen eine role von image und ein aria-label gegeben.

Dies erzeugt den folgenden HTML-Code

<h1 role="image" aria-label="We Love to Play">
  <span class="letter">W</span>
  <span class="letter">e</span>
  <span class="letter"> </span>
  <span class="letter">L</span>
  <span class="letter">o</span>
  <span class="letter">v</span>
  <span class="letter">e</span>
  <span class="letter"> </span>
  <span class="letter">t</span>
  <span class="letter">o</span>
  <span class="letter"> </span>
  <span class="letter">P</span>
  <span class="letter">l</span>
  <span class="letter">a</span>
  <span class="letter">y</span>
</h1>

Grundlegende Stile

Wir platzieren die Überschrift in der Mitte ihres Elternelements (in diesem Fall body) mithilfe eines grid-Layouts.

body {
  display: grid;
  place-content: center;
}
Screenshot of grid layout lines around the centrally placed heading when inspecting it with Firefox DevTools.
Die Überschrift dehnt sich nicht über ihr Elternelement aus, um ihre gesamte width abzudecken, sondern wird stattdessen in der Mitte platziert.

Wir können auch einige verschönernde Elemente hinzufügen, wie eine schöne font oder einen background auf dem Container.

Als nächstes erstellen wir die Linie mit einem absolut positionierten ::after-Pseudo-Element mit einer Dicke (height) von $h.

$h: .125em;
$r: .5*$h;

h1 {
  position: relative;
  
  &::after {
    position: absolute;
    top: calc(50% - #{$r}); right: 0;
    height: $h;
    border-radius: 0 $r $r 0;
    background: crimson;
  }
}

Der obige Code kümmert sich um die Positionierung und die height des Pseudo-Elements, aber was ist mit der width? Wie bringen wir es dazu, vom linken Rand des Viewports bis zum rechten Rand des Überschriftentextes zu reichen?

Linienlänge

Nun, da wir ein grid-Layout haben, bei dem die Überschrift horizontal zentriert ist, bedeutet dies, dass die vertikale Mittellinie des Viewports mit der der Überschrift übereinstimmt und beide in zwei gleich breite Hälften teilt.

SVG illustration. Shows how the vertical midline of the viewport coincides with that of the heading and splits both into equal width halves.
Die zentrierte Überschrift.

Folglich ist der Abstand zwischen dem linken Rand des Viewports und dem rechten Rand der Überschrift die halbe Viewport-Breite (50vw) plus die halbe Überschriftenbreite, was als %-Wert ausgedrückt werden kann, wenn er bei der Berechnung der width des entsprechenden Pseudo-Elements verwendet wird.

Die width unseres ::after-Pseudo-Elements ist also:

width: calc(50vw + 50%);

Die Linie über und unter den Buchstaben verlaufen lassen

Bisher ist das Ergebnis nur eine karminrote Linie, die schwarzen Text durchquert.

Wir möchten, dass einige der Buchstaben über der Linie erscheinen. Um diesen Effekt zu erzielen, geben wir ihnen (oder auch nicht) zufällig eine Klasse von .over. Das bedeutet eine leichte Änderung des Pug-Codes.

- let text = 'We Love to Play';
- let arr = text.split('');

h1(role='image' aria-label=text)
  - arr.forEach(letter => {
    span.letter(class=Math.random() > .5 ? 'over' : null) #{letter}
  - });

Wir positionieren die Buchstaben mit der Klasse .over relativ und geben ihnen einen positiven z-index.

.over {
  position: relative;
  z-index: 1;
}

Meine ursprüngliche Idee beinhaltete die Verwendung von translatez(1px) anstelle von z-index: 1, aber dann fiel mir ein, dass die Verwendung von z-index sowohl eine bessere Browserunterstützung hat als auch weniger Aufwand bedeutet.

Die Linie verläuft über einigen Buchstaben, aber unter anderen.

Animieren!

Jetzt, da wir den kniffligen Teil hinter uns haben, können wir auch eine animation hinzufügen, damit die Linie einfährt. Das bedeutet, dass die karminrote Linie zu Beginn um ihre volle width (100%) nach links (in negativer Richtung der x-Achse, also mit Minuszeichen) verschoben wird, nur um sie dann wieder in ihre normale Position zurückkehren zu lassen.

@keyframes slide { 0% { transform: translate(-100%); } }

Ich habe mich entschieden, vor dem Start der animation etwas Zeit zum Durchatmen zu lassen. Das bedeutete, dass ich die Verzögerung von 1s hinzugefügt habe, was wiederum bedeutete, dass ich das Schlüsselwort backwards für animation-fill-mode hinzugefügt habe, damit die Linie im Zustand bleibt, der durch den 0%-Keyframe vor dem Start der animation definiert ist.

animation: slide 2s ease-out 1s backwards;

Eine 3D-Note

Das brachte mich auf eine weitere Idee: die Linie jeden einzelnen Buchstaben durchlaufen zu lassen, das heißt, über dem Buchstaben zu beginnen, durch ihn hindurchzugehen und darunter zu enden (oder umgekehrt).

Dies erfordert echtes 3D und einige kleine Anpassungen.

Zuerst setzen wir transform-style auf preserve-3d auf der Überschrift, da wir möchten, dass alle ihre Kinder (und Pseudo-Elemente) Teil derselben 3D-Anordnung sind, was bewirkt, dass sie entsprechend ihrer Positionierung in 3D geordnet und überlappend sind.

Als nächstes wollen wir jeden Buchstaben um seine y-Achse drehen, wobei die Drehrichtung von der Anwesenheit der zufällig zugewiesenen Klasse abhängt (deren Namen wir von „over“ in .rev für „reverse“ ändern, da „over“ nicht mehr wirklich darauf hindeutet, was wir hier tun).

Bevor wir dies jedoch tun, müssen wir bedenken, dass unsere Span-Elemente zu diesem Zeitpunkt immer noch Inline-Elemente sind und das Setzen eines transform auf einem Inline-Element keinerlei Wirkung hat.

Um dieses Problem zu lösen, setzen wir display: flex auf die Überschrift. Dies schafft jedoch ein neues Problem: Span-Elemente, die nur ein Leerzeichen enthalten (" "), werden auf eine Breite von null gequetscht.

Screenshot showing how the span containing only a space gets squished to zero width when setting `display: flex` on its parent.
Inspektion eines reinen Leerzeichen-<span> in Firefox DevTools.

Eine einfache Lösung dafür ist, white-space: pre auf unsere .letter-Spans zu setzen.

Sobald wir dies getan haben, können wir unsere Spans um einen Winkel $a drehen... in die eine oder andere Richtung!

$a: 2deg;

.letter {
  white-space: pre;
  transform: rotatey($a);
}

.rev { transform: rotatey(-$a); }

Da die Drehung um die y-Achse unsere Buchstaben horizontal staucht, können wir sie entlang der x-Achse um einen Faktor ($f) skalieren, der das Inverse des Kosinus von $a ist.

$a: 2deg;
$f: 1/cos($a)

.letter {
  white-space: pre;
  transform: rotatey($a) scalex($f)
}

.rev { transform: rotatey(-$a) scalex($f) }

Wenn Sie verstehen möchten, warum dieser spezielle Skalierungsfaktor verwendet wird, können Sie sich diesen älteren Artikel ansehen, in dem ich alles im Detail erkläre.

Und das war's! Wir haben nun das 3D-Ergebnis, das wir angestrebt haben! Beachten Sie jedoch, dass die hier verwendete Schriftart so gewählt wurde, dass unser Ergebnis gut aussieht, und eine andere Schriftart möglicherweise nicht so gut funktioniert.