Was macht Frontend UI Engineering?
Überblick
Erstellen Sie produktionsreife Benutzeroberflächen, die barrierefrei, performant und visuell ausgefeilt sind. Das Ziel ist eine UI, die aussieht, als wäre sie von einem designaffinen Ingenieur eines Top-Unternehmens entwickelt worden – nicht wie von einer KI generiert. Das bedeutet echte Einhaltung von Designsystemen, ordnungsgemäße Barrierefreiheit, durchdachte Interaktionsmuster und keine generische „KI-Ästhetik".
Wann verwenden
- Erstellen neuer UI-Komponenten oder Seiten
- Ändern bestehender benutzerseitiger Oberflächen
- Implementieren responsiver Layouts
- Hinzufügen von Interaktivität oder State-Management
- Beheben visueller oder UX-Probleme
Komponenten-Architektur
Dateistruktur
Alles, was zu einer Komponente gehört, zusammen ablegen:
src/components/
TaskList/
TaskList.tsx # Komponenten-Implementierung
TaskList.test.tsx # Tests
TaskList.stories.tsx # Storybook-Stories (falls verwendet)
use-task-list.ts # Custom Hook (bei komplexem State)
types.ts # Komponentenspezifische Typen (falls nötig)
Komponenten-Muster
Komposition gegenüber Konfiguration bevorzugen:
// Gut: Zusammensetzbar
<Card>
<CardHeader>
<CardTitle>Aufgaben</CardTitle>
</CardHeader>
<CardBody>
<TaskList tasks={tasks} />
</CardBody>
</Card>
// Vermeiden: Überkonfiguriert
<Card
title="Aufgaben"
headerVariant="large"
bodyPadding="md"
content={<TaskList tasks={tasks} />}
/>
Komponenten fokussiert halten:
// Gut: Macht eine Sache
export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
return (
<li className="flex items-center gap-3 p-3">
<Checkbox checked={task.done} onChange={() => onToggle(task.id)} />
<span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>
<Button variant="ghost" size="sm" onClick={() => onDelete(task.id)}>
<TrashIcon />
</Button>
</li>
);
}
Datenabruf von der Darstellung trennen:
// Container: kümmert sich um Daten
export function TaskListContainer() {
const { tasks, isLoading, error } = useTasks();
if (isLoading) return <TaskListSkeleton />;
if (error) return <ErrorState message="Aufgaben konnten nicht geladen werden" retry={refetch} />;
if (tasks.length === 0) return <EmptyState message="Noch keine Aufgaben" />;
return <TaskList tasks={tasks} />;
}
// Presentation: kümmert sich ums Rendern
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<ul role="list" className="divide-y">
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</ul>
);
}
State-Management
Den einfachsten Ansatz wählen, der funktioniert:
Lokaler State (useState) → Komponentenspezifischer UI-State
Hochgezogener State (Lifted State) → Gemeinsam genutzt von 2-3 Geschwisterkomponenten
Context → Theme, Auth, Locale (leseintensiv, schreibarm)
URL-State (searchParams) → Filter, Paginierung, teilbarer UI-State
Server-State (React Query, SWR) → Remote-Daten mit Caching
Globaler Store (Zustand, Redux) → Komplexer Client-State, der appweit geteilt wird
Prop Drilling tiefer als 3 Ebenen vermeiden. Wenn Sie Props durch Komponenten reichen, die sie nicht verwenden, führen Sie Context ein oder strukturieren Sie den Komponentenbaum um.
Einhaltung des Designsystems
Referenzgeführte UI-Qualität
Wenn das Produkt eine klare visuelle Richtung benötigt, sammeln Sie Belege, bevor Sie ein Layout auswählen:
- Durchsuchen Sie einen vertrauenswürdigen Referenzkatalog oder verwenden Sie Referenzen, die vom Produktteam bereitgestellt werden.
- Studieren Sie zwei oder drei relevante Bildschirme. Halten Sie Entscheidungen zu Hierarchie, Dichte, Navigation, Bedienelementen, responsivem Verhalten und Interaktionszuständen fest.
- Verwandeln Sie diese Entscheidungen vor der Implementierung in einen kurzen Designvertrag. Benennen Sie die Aufgabe des Bildschirms, die primäre Aktion, die erforderlichen Zustände, die responsiven Regeln und die abzulehnenden Muster.
- Bauen Sie die nützliche Struktur mit den eigenen Komponenten, Tokens, Inhalten und der visuellen Sprache des Produkts nach. Kopieren Sie niemals das Branding, proprietäre Texte, Bilder oder das genaue Layout eines anderen Produkts.
Verwenden Sie Referenzen als Belege, nicht als Vorlagen. Wenn keine Referenzen verfügbar sind, dokumentieren Sie die Annahmen und überprüfen Sie das Ergebnis mit dem bestehenden Designsystem des Produkts.
Die KI-Ästhetik vermeiden
KI-generierte UI hat erkennbare Muster. Vermeiden Sie alle:
| KI-Standard | Warum es ein Problem ist | Produktionsqualität |
|---|---|---|
| Lila/Indigo überall | Modelle greifen standardmäßig zu visuell „sicheren" Paletten, was jede App gleich aussehen lässt | Die tatsächliche Farbpalette des Projekts verwenden |
| Übermäßige Verläufe | Verläufe fügen visuelles Rauschen hinzu und kollidieren mit den meisten Designsystemen | Flache oder subtile Verläufe, die zum Designsystem passen |
| Alles abgerundet (rounded-2xl) | Maximale Abrundung signalisiert „freundlich", ignoriert aber die Hierarchie der Eckenradien in echten Designs | Einheitlicher border-radius aus dem Designsystem |
| Generische Hero-Sektionen | Vorlagengetriebenes Layout ohne Bezug zum tatsächlichen Inhalt oder Benutzerbedarf | Inhaltsorientierte Layouts |
| Lorem-ipsum-artiger Text | Platzhaltertext versteckt Layoutprobleme, die echter Inhalt offenbart (Länge, Umbruch, Überlauf) | Realistische Platzhalterinhalte |
| Übergroße Abstände überall | Gleichmäßig großzügige Abstände zerstören die visuelle Hierarchie und verschwenden Bildschirmfläche | Konsistente Abstandsskala |
| Standard-Kartenraster | Einheitliche Raster sind ein Layout-Kürzel, das die Informationspriorität und Scan-Muster ignoriert | Zweckorientierte Layouts |
| Schattenlastiges Design | Mehrere Schattenebenen erzeugen Tiefe, die mit dem Inhalt konkurriert und das Rendern auf schwächeren Geräten verlangsamt | Dezente oder keine Schatten, es sei denn, das Designsystem gibt sie vor |
Abstände und Layout
Verwenden Sie eine konsistente Abstandsskala. Erfinden Sie keine Werte:
/* Die Skala verwenden: 0.25rem-Schritte (oder was das Projekt verwendet) */
/* Gut */ padding: 1rem; /* 16px */
/* Gut */ gap: 0.75rem; /* 12px */
/* Schlecht */ padding: 13px; /* Nicht in der Skala */
/* Schlecht */ margin-top: 2.3rem; /* Nicht in der Skala */
Typografie
Die Typografie-Hierarchie respektieren:
h1 → Seitentitel (eine pro Seite)
h2 → Abschnittstitel
h3 → Unterabschnittstitel
body → Standardtext
small → Sekundär-/Hilfstext
Überspringen Sie keine Überschriftenebenen. Verwenden Sie keine Überschriften-Stile für Nicht-Überschriften-Inhalte.
Farbe
- Semantische Farb-Tokens verwenden:
text-primary,bg-surface,border-default– keine rohen Hex-Werte - Ausreichenden Kontrast sicherstellen (4.5:1 für normalen Text, 3:1 für großen Text)
- Nicht ausschließlich auf Farbe zur Informationsvermittlung verlassen (auch Icons, Text oder Muster verwenden)
Barrierefreiheit (WCAG 2.1 AA)
Jede Komponente muss diese Standards erfüllen:
Tastaturnavigation
// Jedes interaktive Element muss per Tastatur erreichbar sein
<button onClick={handleClick}>Klick mich</button> // ✓ Standardmäßig fokussierbar
<div onClick={handleClick}>Klick mich</div> // ✗ Nicht fokussierbar
<div role="button" tabIndex={0} onClick={handleClick} // ✓ Aber <button> bevorzugen
onKeyDown={e => {
if (e.key === 'Enter') handleClick();
if (e.key === ' ') e.preventDefault();
}}
onKeyUp={e => {
if (e.key === ' ') handleClick();
}}>
Klick mich
</div>
ARIA-Labels
// Interaktive Elemente ohne sichtbaren Text kennzeichnen
<button aria-label="Dialog schließen"><XIcon /></button>
// Formulareingaben kennzeichnen
<label htmlFor="email">E-Mail</label>
<input id="email" type="email" />
// Oder aria-label verwenden, wenn kein sichtbares Label vorhanden ist
<input aria-label="Aufgaben suchen" type="search" />
Fokus-Management
// Fokus verschieben, wenn sich der Inhalt ändert
function Dialog({ isOpen, onClose }: DialogProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
// Fokus im Dialog gefangen halten, wenn geöffnet
return (
<dialog open={isOpen}>
<button ref={closeRef} onClick={onClose}>Schließen</button>
{/* Dialog-Inhalt */}
</dialog>
);
}
Aussagekräftige Leer- und Fehlerzustände
// Keine leeren Bildschirme anzeigen
function TaskList({ tasks }: { tasks: Task[] }) {
if (tasks.length === 0) {
return (
<div role="status" className="text-center py-12">
<TasksEmptyIcon className="mx-auto h-12 w-12 text-muted" />
<h3 className="mt-2 text-sm font-medium">Keine Aufgaben</h3>
<p className="mt-1 text-sm text-muted">Erstellen Sie eine neue Aufgabe, um zu beginnen.</p>
<Button className="mt-4" onClick={onCreateTask}>Aufgabe erstellen</Button>
</div>
);
}
return <ul role="list">...</ul>;
}
Responsive Design
Zuerst für Mobilgeräte entwerfen, dann erweitern:
// Tailwind: Mobile-First responsiv
<div className="
grid grid-cols-1 /* Mobil: Einspaltig */
sm:grid-cols-2 /* Klein: 2 Spalten */
lg:grid-cols-3 /* Groß: 3 Spalten */
gap-4
">
An diesen Breakpoints testen: 320px, 768px, 1024px, 1440px.
Ladezustände und Übergänge
// Skeleton-Ladezustand (keine Spinner für Inhalte)
function TaskListSkeleton() {
return (
<div className="space-y-3" aria-busy="true" aria-label="Aufgaben werden geladen">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-12 bg-muted animate-pulse rounded" />
))}
</div>
);
}
// Optimistische Updates für gefühlte Geschwindigkeit
function useToggleTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleTask,
onMutate: async (taskId) => {
await queryClient.cancelQueries({ queryKey: ['tasks'] });
const previous = queryClient.getQueryData(['tasks']);
queryClient.setQueryData(['tasks'], (old: Task[]) =>
old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)
);
return { previous };
},
onError: (_err, _taskId, context) => {
queryClient.setQueryData(['tasks'], context?.previous);
},
});
}
Siehe auch
Eine detaillierte Checkliste zu Barrierefreiheitsanforderungen und Testwerkzeugen finden Sie unter ../../references/accessibility-checklist.md.
Häufige Rechtfertigungen
| Rechtfertigung | Realität |
|---|---|
| „Barrierefreiheit ist ein Nice-to-have" | Sie ist in vielen Rechtsordnungen eine gesetzliche Anforderung und ein technischer Qualitätsstandard. |
| „Das Responsive Design machen wir später" | Responsive Design nachzurüsten ist 3x schwieriger, als es von Anfang an zu bauen. |
| „Das Design ist noch nicht fertig, also überspringe ich das Styling" | Verwenden Sie die Designsystem-Standardwerte. Ungestylte UI hinterlässt einen schlechten ersten Eindruck bei Prüfern. |
| „Das ist nur ein Prototyp" | Prototypen werden zu Produktionscode. Bauen Sie das Fundament richtig. |
| „Die KI-Ästhetik ist fürs Erste in Ordnung" | Sie signalisiert geringe Qualität. Verwenden Sie von Anfang an das eigentliche Designsystem des Projekts. |
Warnsignale
- Komponenten mit mehr als 200 Zeilen (teilen Sie sie auf)
- Inline-Styles oder willkürliche Pixelwerte
- Fehlende Fehler-, Lade- oder Leerzustände
- Keine Tastaturnavigation getestet
- Farbe als alleiniger Indikator für Zustand (rot/grün ohne Text oder Icons)
- Generischer „KI-Look" (lila Verläufe, überdimensionierte Karten, Standard-Layouts)
Überprüfung
Nachdem Sie die UI erstellt haben:
- Die Komponente rendert ohne Konsolenfehler
- Alle interaktiven Elemente sind per Tastatur erreichbar (Tab durch die Seite)
- Der Screenreader kann den Inhalt und die Struktur der Seite vermitteln
- Responsiv: funktioniert bei 320px, 768px, 1024px, 1440px
- Lade-, Leer-, Fehler-, Erfolgs- und Berechtigungszustände werden behandelt, wenn zutreffend
- Das Designsystem des Projekts wird eingehalten (Abstände, Farben, Typografie)
- Das gerenderte Ergebnis besteht eine abschließende UI-spezifische Qualitätskontrolle
- Keine Barrierefreiheitswarnungen in den DevTools oder von axe-core