Ich bin fest davon überzeugt, dass SVG eine ganze Welt des Interface-Designs im Web eröffnet. Es mag anfangs einschüchternd wirken, SVG zu lernen, aber es gibt eine Spezifikation, die zum Erstellen von Formen konzipiert wurde und dennoch Elemente wie Text, Links und Aria-Labels bereitstellt. Man kann ähnliche Effekte mit CSS erzielen, aber es ist etwas kniffliger, die Positionierung, insbesondere über verschiedene Ansichten und für responsives Design, perfekt hinzubekommen.
Das Besondere an SVG ist, dass die gesamte Positionierung auf einem Koordinatensystem basiert, ähnlich wie beim Spiel Schiffe versenken. Das bedeutet, dass die Entscheidung, wo alles platziert und wie es gezeichnet wird, sowie wie es sich zueinander verhält, wirklich einfach zu durchdenken ist. CSS-Positionierung ist für das Layout gedacht, was großartig ist, da die Elemente im Fluss des Dokuments miteinander korrespondieren. Dieser sonst positive Aspekt ist jedoch schwieriger zu handhaben, wenn man eine sehr spezifische Komponente mit überlappenden und präzise platzierten Elementen erstellt.
Wirklich, sobald Sie SVG beherrschen, können Sie alles zeichnen und es auf jedem Gerät skalieren lassen. Selbst diese Seite verwendet SVG für benutzerdefinierte UI-Elemente, wie meinen Avatar oben (Meta!).

Wir werden in diesem Beitrag nicht *alles* über SVG behandeln (einige Grundlagen dazu lernen Sie hier, hier, hier und hier), aber um die Möglichkeiten aufzuzeigen, die SVG für die Entwicklung von UI-Komponenten eröffnet, sprechen wir über einen bestimmten Anwendungsfall und analysieren, wie wir über den Aufbau von etwas Benutzerdefiniertem nachdenken würden.
Die Timeline-Task-Listen-Komponente
Vor kurzem habe ich mit meinem Team bei Netlify an einem Projekt gearbeitet. Wir wollten dem Betrachter anzeigen, welches Video in einer Reihe von Videos eines Kurses gerade angesehen wird. Mit anderen Worten, wir wollten so etwas wie eine To-do-Liste erstellen, die jedoch den Gesamtfortschritt anzeigt, wenn Elemente abgeschlossen sind. (Wir haben eine kostenlose Lernplattform im Weltraum-Thema erstellt und sie ist verdammt cool. Ja, ich habe *verdammt* gesagt.)
So sieht das aus

Wie würden wir also vorgehen? Ich zeige ein Beispiel in Vue und React, damit Sie sehen können, wie es in beiden Frameworks funktionieren könnte.
Die Vue-Version
Wir haben uns entschieden, die Plattform in Next.js zu entwickeln, um sie selbst zu testen (d. h. unser eigenes Next on Netlify Build Plugin auszuprobieren), aber da ich besser mit Vue vertraut bin, habe ich den ersten Prototyp in Vue geschrieben und ihn dann nach React portiert.
Hier ist die vollständige CodePen-Demo
Lassen Sie uns diesen Code etwas näher betrachten. Zunächst ist dies eine Single-File-Komponente (SFC), sodass das Template-HTML, das reaktive Skript und die scoped Styles in dieser einen Datei gekapselt sind.
Wir speichern einige Dummy-Aufgaben in data, einschließlich, ob jede Aufgabe erledigt ist oder nicht. Wir erstellen auch eine Methode, die wir über eine Click-Direktive aufrufen können, um den Status auf "erledigt" oder "nicht erledigt" umzuschalten.
<script>
export default {
data() {
return {
tasks: [
{
name: 'thing',
done: false
},
// ...
]
};
},
methods: {
selectThis(index) {
this.tasks[index].done = !this.tasks[index].done
}
}
};
</script>
Nun wollen wir ein SVG erstellen, das je nach Anzahl der Elemente eine flexible viewBox hat. Wir wollen auch Screenreadern mitteilen, dass es sich um ein präsentes Element handelt und dass wir einen Titel mit der eindeutigen ID timeline bereitstellen werden. (Weitere Informationen zum Erstellen von zugänglichen SVGs.)
<template>
<div id="app">
<div>
<svg :viewBox="`0 0 30 ${tasks.length * 50}`"
xmlns="http://www.w3.org/2000/svg"
width="30"
stroke="currentColor"
fill="white"
aria-labelledby="timeline"
role="presentation">
<title id="timeline">timeline element</title>
<!-- ... -->
</svg>
</div>
</div>
</template>
Die stroke ist auf currentColor eingestellt, um Flexibilität zu ermöglichen – wenn wir die Komponente an mehreren Stellen wiederverwenden möchten, erbt sie die color, die auf dem umschließenden Div verwendet wird.
Als Nächstes wollen wir innerhalb des SVG eine vertikale Linie erstellen, die die Länge der Aufgabenliste hat. Linien sind ziemlich einfach. Wir haben x1 und x2 Werte (wo die Linie auf der x-Achse gezeichnet wird) und ähnlich y1 und y2.
<line x1="10" x2="10" :y1="num2" :y2="tasks.length * num1 - num2" />
Die x-Achse bleibt konstant bei 10, da wir eine Linie nach unten und nicht von links nach rechts zeichnen. Wir speichern zwei Zahlen in data: den Betrag, den wir für den Abstand wünschen, welcher num1 sein wird, und den Betrag, den wir für den Rand wünschen, welcher num2 sein wird.
data() {
return {
num1: 32,
num2: 15,
// ...
}
}
Die y-Achse beginnt mit num2, das vom Ende abgezogen wird, sowie dem Rand. Die tasks.length wird mit dem Abstand (num1) multipliziert.
Jetzt benötigen wir die Kreise, die auf der Linie liegen. Jeder Kreis ist ein Indikator dafür, ob eine Aufgabe abgeschlossen wurde oder nicht. Wir benötigen einen Kreis für jede Aufgabe, daher verwenden wir v-for mit einem eindeutigen key, der der Index ist (und hier sicher verwendet werden kann, da sie niemals neu geordnet werden). Wir verbinden die click-Direktive mit unserer Methode und übergeben den Index als Parameter.
Kreise in SVG bestehen aus drei Attributen. Die Mitte des Kreises wird bei cx und cy platziert, und dann zeichnen wir einen Radius mit r. Wie die Linie beginnt cx bei 10. Der Radius ist 4, da dies in diesem Maßstab gut lesbar ist. cy wird wie die Linie beabstandet: Index mal Abstand (num1) plus Rand (num2).
Schließlich verwenden wir einen Ternary-Operator, um die fill-Eigenschaft festzulegen. Wenn die Aufgabe erledigt ist, wird sie mit currentColor gefüllt. Wenn nicht, wird sie mit white (oder was auch immer der Hintergrund ist) gefüllt. Dies könnte beispielsweise mit einer Prop gefüllt werden, die im Hintergrund übergeben wird, wo Sie helle und dunkle Kreise haben.
<circle
@click="selectThis(i)"
v-for="(task, i) in tasks"
:key="task.name"
cx="10"
r="4"
:cy="i * num1 + num2"
:fill="task.done ? 'currentColor' : 'white'"
class="select"/>
Schließlich verwenden wir CSS Grid, um ein Div mit den Aufgabennamen auszurichten. Dies wird auf ähnliche Weise gestaltet, indem wir durch die Aufgaben iterieren und ebenfalls an dieselbe Klick-Veranstaltung gebunden sind, um den erledigten Status umzuschalten.
<template>
<div>
<div
@click="selectThis(i)"
v-for="(task, i) in tasks"
:key="task.name"
class="select">
{{ task.name }}
</div>
</div>
</template>
Die React-Version
Hier ist, wo wir mit der React-Version gelandet sind. Wir arbeiten daran, dies Open Source zu machen, damit Sie den vollständigen Code und seine Historie sehen können. Hier sind einige Modifikationen:
- Wir verwenden CSS-Module anstelle der SFCs in Vue.
- Wir importieren den Next.js-Link, sodass anstelle des Umschaltens eines "erledigt"-Status die Benutzer zu einer dynamischen Seite in Next.js weitergeleitet werden.
- Die verwendeten Aufgaben sind tatsächlich Phasen des Kurses – oder "Mission", wie wir sie nennen – die hier übergeben werden, anstatt vom Komponenten gehalten zu werden.
Der Großteil der anderen Funktionalität ist gleich :)
import styles from './MissionTracker.module.css';
import React, { useState } from 'react';
import Link from 'next/link';
function MissionTracker({ currentMission, currentStage, stages }) {
const [tasks, setTasks] = useState([...stages]);
const num1 = 32;
const num2 = 15;
const updateDoneTasks = (index) => () => {
let tasksCopy = [...tasks];
tasksCopy[index].done = !tasksCopy[index].done;
setTasks(tasksCopy);
};
const taskTextStyles = (task) => {
const baseStyles = `${styles['tracker-select']} ${styles['task-label']}`;
if (currentStage === task.slug.current) {
return baseStyles + ` ${styles['is-current-task']}`;
} else {
return baseStyles;
}
};
return (
<div className={styles.container}>
<section>
{tasks.map((task, index) => (
<div
key={`mt-${task.slug}-${index}`}
className={taskTextStyles(task)}
>
<Link href={`/learn/${currentMission}/${task.slug.current}`}>
{task.title}
</Link>
</div>
))}
</section>
<section>
<svg
viewBox={`0 0 30 ${tasks.length * 50}`}
className={styles['tracker-svg']}
xmlns="http://www.w3.org/2000/svg"
width="30"
stroke="currentColor"
fill="white"
aria-labelledby="timeline"
role="presentation"
>
<title id="timeline">timeline element</title>
<line x1="10" x2="10" y1={num2} y2={tasks.length * num1 - num2} />
{tasks.map((task, index) => (
<circle
key={`mt-circle-${task.name}-${index}`}
onClick={updateDoneTasks(index)}
cx="10"
r="4"
cy={index * +num1 + +num2}
fill={
task.slug.current === currentStage ? 'currentColor' : 'black'
}
className={styles['tracker-select']}
/>
))}
</svg>
</section>
</div>
);
}
export default MissionTracker;
Finale Version
Hier sehen Sie die endgültige, funktionierende Version
Diese Komponente ist flexibel genug, um Listen kleiner und großer Umfänge, verschiedene Browser und responsive Größen zu berücksichtigen. Sie ermöglicht dem Benutzer auch ein besseres Verständnis seines Fortschritts im Kurs.
Aber das ist nur eine Komponente. Sie können beliebig viele UI-Elemente erstellen: Knöpfe, Steuerelemente, Fortschrittsanzeigen, Ladeanimationen… der Fantasie sind keine Grenzen gesetzt. Sie können sie mit CSS oder Inline-Styles gestalten, sie können auf Props, Kontext oder reaktive Daten reagieren, der Fantasie sind keine Grenzen gesetzt! Ich hoffe, das eröffnet Ihnen neue Perspektiven für die Entwicklung von ansprechenderen UI-Elementen für das Web.
Danke, Sarah!
Ich verwende SVG schon seit einiger Zeit.
Hier ist meine Meinung dazu:
Ein besserer Ansatz wäre, die Komponente zuerst mit einem visuellen Werkzeug wie Adobe Illustrator oder Inkscape zu entwerfen und sie dann mit JS zu manipulieren oder ihr Interaktivität hinzuzufügen.
Auf diese Weise ist es bequemer.
Toller Artikel, Sarah, ich habe es sehr genossen und schätze, was Ihr Team dafür geleistet hat.
@vishal: Ich erstelle auch seit Jahren Komponenten mit SVG, und Sie haben Recht, dass es manchmal schneller ist, etwas in Illustrator zu erstellen. In diesem Fall muss die Komponente jedoch flexibel für viele Elemente sein, und daher müssen Sie Längen dynamisch mit JS erstellen und binden. Dieser Weg ist also für viele Anwendungsfälle nicht flexibel genug. Sie müssen auch einiges davon von Hand schreiben, um es zugänglich zu machen. Ich möchte ganz klarstellen, dass mein Team diese Komponente nicht geschrieben hat, sondern ich. Daher ist dies keine reine Aufbereitung der Arbeit anderer. Der Artikel enthält auch eine Erklärung, warum ich diesen Ansatz gewählt habe. Cheers!
Toller Artikel. Ich habe vor einiger Zeit mit Ihrem SVG-Buch begonnen und sehe immer wieder Möglichkeiten dafür. Schön, das hier zu sehen und jemanden, der mir dabei auch etwas Vue beibringt.
titleinnerhalb des Inhalts – habe ich etwas zur Verwendung dieses Elements verpasst?Das SVG-Element hat ein eigenes Titel-Tag, nicht das Dokument-Titel-Tag, an das Sie wahrscheinlich denken.
Das ist großartig!
Ich bin neugierig auf etwas in Ihrem Beispielcode, das mich stutzig gemacht hat.
anstelle von
Ich erwartete, dass die späteren arithmetischen Operationen auf seltsame Weise reagieren würden, wenn sie ein Array erhalten, aber sie funktionieren perfekt! Solange Sie nur ein Element im Array haben. Ich vermute, dass so etwas wie eine Typumwandlung von String zu Zahl und zurück stattfindet.
Macht es das Erstellen eines Einzelelement-Arrays mit einer Zahl darin einfacher oder hat es andere Vorteile, als eine Zahl zu verwenden, oder ist es eine stilistische Entscheidung? (Oder etwas anderes?)
Hey! Tatsächlich war das ein Überbleibsel einer älteren Iteration und ich hatte es aktualisiert, aber es wurde irgendwie zurückgesetzt. Danke, dass Sie es erwähnt haben! Es sollte jetzt behoben sein :)
Danke für den Artikel, Sarah!
Was sind die Vor- und Nachteile der Erstellung dieser Komponente in HTML/CSS anstelle von SVG?
Danke!
Toller Artikel, Sarah! :) Nur ein Gedanke: Vorsicht vor dem Verändern von State in React. Das sollte *immer* vermieden werden. Wenn Sie dies tun:
Sie verändern tatsächlich
tasks[index](da[...tasks]nur eine flache Kopie des Arrays ist). Sie sollten stattdessen so etwas tun:tasksCopy[index] = {...tasksCopy[index], done: tasksCopy[index].done}Es wäre vielleicht besser gewesen,
spacingundmarginanstelle vonnum1undnum2zu verwenden. Diesen Teil des Codes fand ich verwirrend. Ich bin mir auch nicht sicher, was der Unterschied zwischen diesen beiden ist, daspacingundmarginsemantisch sehr ähnlich klingen. Ansonsten ein cooler Tutorial!Die Sache ist, dass die verschiedenen Vue- und React-Erweiterungen den Code kaum noch als Standard-JS erkennbar machen.
Es scheint, dass einige Leute hier mit Ihnen streiten, und ich wollte nur sagen: Danke, schönes Beispiel, und ich finde das inspirierend! Ich habe mich in letzter Zeit mit SVG in React beschäftigt, ich stimme zu, dass sie eine perfekte Ergänzung für die UI-Gestaltung sind. Ich freue mich auf mehr von Ihnen!