# Benutzeroberflächen entwerfen und umsetzen

> **Die Abschnitte „In Antiquariat Kassius" zeigen echten Code** aus meinem Projekt Antiquariat Kassius, letzter Stand vom Mai 2026. Das Antiquariat ist in zwei Modulen entstanden: hier in M322 der Style-Guide und die Oberfläche, in M295 das Backend dahinter. [Zur Projektseite](/projects/antiquariat.html) · [Zum Backend in M295](/module/m295.html)

## Inhaltsverzeichnis

- [Zuerst planen, dann bauen](#zuerst-planen-dann-bauen)
- [Software-Ergonomie](#software-ergonomie)
- [Der Style-Guide](#der-style-guide)
- [Komponenten statt Einzelstücke](#komponenten-statt-einzelstücke)
- [Navigation: Wo bin ich?](#navigation-wo-bin-ich)
- [Formulare](#formulare)
- [Suchen und Filtern als Oberfläche](#suchen-und-filtern-als-oberfläche)
- [Tabellen im Admin-Bereich](#tabellen-im-admin-bereich)
- [Rückmeldung und Löschdialog](#rückmeldung-und-löschdialog)
- [Barrierefreiheit](#barrierefreiheit)
- [Zusammenfassung / Spickzettel](#zusammenfassung--spickzettel)

---

## Zuerst planen, dann bauen

In M322 geht es darum, eine Oberfläche zu planen, bevor sie programmiert wird. Beim Antiquariat habe ich deshalb zuerst in Figma alle Masken gestaltet und daraus einen Style-Guide abgeleitet. Erst danach kam der Code. Der erste Schritt ist eine **Seitenübersicht**: Welche Seiten gibt es, und wer darf sie sehen?

```
Öffentlicher Bereich            Admin-Bereich (nur nach Login)
├── Startseite                  ├── Dashboard
├── Unsere Sammlung             ├── Bücher verwalten
├── Buchdetail                  ├── Kunden verwalten
└── Admin-Login                 └── Passwort ändern
```

Der Style-Guide verlangt eine klare Trennung der beiden Bereiche. Im öffentlichen Teil liegt die Navigation oben und bleibt auf jeder Seite gleich. Im Admin-Bereich kommt links eine feste Sidebar dazu, über die man zwischen Dashboard, Bücherverwaltung, Kundenverwaltung und Passwort wechselt, ohne zurück auf eine Übersicht zu müssen.

## Software-Ergonomie

Eine Oberfläche ist ergonomisch, wenn man sie ohne Anleitung richtig bedienen kann und nicht erst lernen muss, wo was liegt. Für das Antiquariat habe ich fünf Punkte festgelegt und im Dossier beschrieben. So sehen sie in der fertigen Anwendung aus:

| Punkt | Im Antiquariat |
|---|---|
| Übersichtlichkeit | Suche und Filter in einem eigenen Kasten, Bücher als Karten, Verwaltung als Tabelle |
| Einheitliches Design | Alle Farben kommen aus Variablen, Knöpfe, Karten und Felder haben denselben Radius von 8px |
| Klare Navigation | Der aktive Menüpunkt ist markiert, der Admin-Bereich hat eine feste Sidebar |
| Feedback bei Aktionen | Trefferzahl nach jeder Suche, Meldung nach dem Speichern, Rückfrage vor dem Löschen |
| Sicherheit | Der Admin-Bereich ist nur nach dem Login erreichbar, das Passwort lässt sich ändern |

## Der Style-Guide

Ein Style-Guide hält fest, wie die Oberfläche aussieht, damit jede Seite dieselbe Sprache spricht, auch wenn sie erst Wochen später gebaut wird. Meiner für das Antiquariat legt unter anderem fest:

| Bereich | Festlegung |
|---|---|
| Primärfarbe | `#1a3a5c` (dunkles Blau) für Header, Buttons und Sidebar |
| Hintergrund | `#F5F1E8` (helles Beige) |
| Akzentfarbe | `#8B1E3F` (dunkles Rot) für Löschen und aktive Elemente |
| Text | `#1a1a2e`, gedämpfter Text `#6b6b6b` |
| Status | Grün `#16a34a` |
| Typografie | H1 48px, H2 36px, H3 20px, Fliesstext 16px, kleiner Text 14px |
| Abstände | 8, 16, 24, 32 und 64px |
| Ecken | Einheitlich 8px Radius für Buttons, Karten und Eingabefelder |
| Fokus | Eingabefelder bekommen beim Fokus einen Rahmen in der Primärfarbe |

Der helle Hintergrund sorgt für einen deutlichen Kontrast zum dunklen Text. Das Rot ist für Wichtiges reserviert, zum Beispiel das Löschen oder das Abzeichen „Verkauft" auf einem Buch.

### In Antiquariat Kassius: die Farben als Variablen

Die Farben aus dem Style-Guide stehen eins zu eins als CSS-Variablen ganz oben im Stylesheet. Keine Seite schreibt einen Farbwert selbst hin, alle holen ihn von hier. Soll das Blau dunkler werden, ändere ich eine Zeile. Dazu kommen ein gedämpftes Beige für Tabellenköpfe, der Radius und zwei Schatten.

```css
:root {
    --primary: #1a3a5c;
    --accent: #8B1E3F;
    --bg: #F5F1E8;
    --muted: #e8e4d9;
    --text: #1a1a2e;
    --text-muted: #6b6b6b;
    --white: #ffffff;
    --green: #16a34a;
    --radius: 8px;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.10);
    --shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.15);
}
```

## Komponenten statt Einzelstücke

Ein Knopf sieht auf jeder Seite gleich aus, wenn er nicht jedes Mal neu gestaltet wird. Deshalb gibt es eine Grundklasse `.btn`, die Form, Abstand und Schrift festlegt, und Varianten, die nur die Farbe ändern. Im HTML kombiniert man beides: `class="btn btn-primary"`.

### In Antiquariat Kassius: Buttons

Die Grundklasse hat immer einen Rahmen von 2px, bei den gefüllten Varianten ist er nur durchsichtig. Dadurch ist ein Outline-Knopf genau gleich gross wie ein gefüllter, und die Knöpfe in einer Reihe stehen sauber nebeneinander.

```css
.btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.55rem 1.25rem;
    border-radius: var(--radius);
    font-size: 0.95rem;
    font-weight: 500;
    cursor: pointer;
    border: 2px solid transparent;
    transition: all 0.2s;
    line-height: 1.4;
}

.btn-primary {
    background: var(--primary);
    color: var(--white);
}

.btn-primary:hover {
    background: #14304f;
}

.btn-accent {
    background: var(--accent);
    color: var(--white);
}

.btn-accent:hover {
    background: #751a35;
}

.btn-outline {
    background: transparent;
    border-color: var(--primary);
    color: var(--primary);
}

.btn-outline:hover {
    background: var(--primary);
    color: var(--white);
}
```

Wie gut das zusammenspielt, zeigt die Seitennavigation unter der Bücherliste im Admin-Bereich. Jede Seitenzahl ist ein kleiner Knopf, die aktuelle Seite ist gefüllt, alle anderen sind Outline. Eine Zeile PHP entscheidet, welche Variante dazukommt.

```php
<?php for ($p = max(1, $page - 2); $p <= min($totalPages, $page + 2); $p++): ?>
    <a href="books.php?<?= $qBase ?>page=<?= $p ?>"
        class="btn btn-sm <?= $p === $page ? 'btn-primary' : 'btn-outline' ?>"><?= $p ?></a>
<?php endfor; ?>
```

## Navigation: Wo bin ich?

Eine gute Navigation beantwortet jederzeit drei Fragen: Wo bin ich, wo kann ich hin, und wie komme ich zurück. Dafür reicht es nicht, die Links hinzuschreiben. Der Link zur aktuellen Seite muss sich von den anderen abheben.

### In Antiquariat Kassius: der aktive Menüpunkt

Die Navigation steht einmal in `includes/header.php` und wird von jeder Seite eingebunden. Welcher Link aktiv ist, entscheidet PHP anhand der aufgerufenen Adresse: Enthält sie `books.php`, bekommt der Link „Bücher" die Klasse `active`. Ob „Admin" und „Abmelden" oder nur „Admin Login" erscheint, hängt davon ab, ob jemand eingeloggt ist.

```php
<nav class="navbar">
    <div class="container navbar-inner">
        <a href="<?= BASE_URL ?>/index.php" class="navbar-brand">Antiquariat Kassius</a>
        <ul class="navbar-links">
            <li><a href="<?= BASE_URL ?>/index.php" <?= str_contains($currentPath, 'index.php') || $currentPath === BASE_URL . '/' ? 'class="active"' : '' ?>>Startseite</a></li>
            <li><a href="<?= BASE_URL ?>/books.php" <?= str_contains($currentPath, 'books.php') ? 'class="active"' : '' ?>>Bücher</a></li>
            <?php if (isLoggedIn()): ?>
                <li><a href="<?= BASE_URL ?>/admin/index.php" <?= str_contains($currentPath, '/admin/') ? 'class="active"' : '' ?>>Admin</a></li>
                <li><a href="<?= BASE_URL ?>/logout.php" class="btn btn-outline-nav">Abmelden</a></li>
            <?php else: ?>
                <li><a href="<?= BASE_URL ?>/login.php" class="btn btn-outline-nav">Admin Login</a></li>
            <?php endif; ?>
        </ul>
    </div>
</nav>
```

Im CSS sind die Links auf dem blauen Balken leicht durchsichtig. Der aktive Link und der, über dem die Maus steht, werden voll weiss:

```css
.navbar-links a {
    color: rgba(255, 255, 255, 0.85);
    font-size: 0.95rem;
    transition: color 0.2s;
}

.navbar-links a:hover,
.navbar-links a.active {
    color: var(--white);
}
```

Die Sidebar im Admin-Bereich funktioniert nach demselben Muster, nur mit einer hell hinterlegten Zeile statt weisser Schrift.

## Formulare

Ein Formular ist dann gut, wenn man es nicht falsch ausfüllen kann. Ein paar Regeln, die dabei helfen:

- Über jedem Feld steht eine Beschriftung, nicht nur ein Platzhalter, der beim Tippen verschwindet
- Pflichtfelder sind mit `*` markiert und haben `required`, damit der Browser leere Felder gar nicht erst abschickt
- Der Feldtyp passt zum Inhalt: `type="number"` für die Katalognummer, `type="email"` für die E-Mail, `type="date"` für Geburtstag und Kundendatum. Der Browser zeigt dann die passende Eingabe, zum Beispiel einen Kalender
- Das Feld, in dem man gerade tippt, ist klar erkennbar

### In Antiquariat Kassius: Fokus und Auswahl

Alle Eingabefelder teilen sich die Klasse `.form-control`. Beim Fokus wird der graue Rahmen blau, wie im Style-Guide festgelegt:

```css
.form-control {
    width: 100%;
    padding: 0.6rem 0.9rem;
    border: 1.5px solid #ccc;
    border-radius: var(--radius);
    font-size: 0.95rem;
    background: var(--white);
    color: var(--text);
    transition: border-color 0.2s;
}

.form-control:focus {
    outline: none;
    border-color: var(--primary);
}
```

Die Kategorie eines Buchs ist Pflicht. Die erste Option ist deshalb leer und heisst „Bitte wählen". Weil das Feld `required` ist, lässt der Browser das Formular erst abschicken, wenn eine echte Kategorie ausgewählt ist. So landet nie aus Versehen die erste Kategorie der Liste in der Datenbank.

```php
<div class="form-group">
    <label>Kategorie *</label>
    <select name="kategorie" class="form-control" required>
        <option value="">– Bitte wählen –</option>
        <?php foreach ($kats as $k): ?>
            <option value="<?= $k['id'] ?>" <?= ($editBook['kategorie'] ?? '') == $k['id'] ? 'selected' : '' ?>><?= h($k['kategorie']) ?></option>
        <?php endforeach; ?>
    </select>
</div>
```

Bei der Kundenverwaltung gibt es eine Checkbox, ob ein Kunde per Mail kontaktiert werden darf. Über `for` und `id` gehören Checkbox und Beschriftung zusammen, deshalb kann man auch auf den Text klicken und muss nicht das kleine Kästchen treffen.

```php
<div class="form-group">
    <div class="checkbox-group">
        <input type="checkbox" id="kontaktpermail" name="kontaktpermail" value="1"
            <?= !empty($editCust['kontaktpermail']) ? 'checked' : '' ?>>
        <label for="kontaktpermail">Kontakt per Mail erlaubt</label>
    </div>
</div>
```

## Suchen und Filtern als Oberfläche

Die Suche nach Büchern ist die wichtigste Funktion für Besucher. Auf der Seite „Unsere Sammlung" steht sie deshalb ganz oben in einem eigenen Kasten: ein Suchfeld mit Knopf, darunter vier Checkboxen, worin gesucht wird, und in einer zweiten Zeile Kategorie, Zustand und Sortierung. Ganz rechts setzt „Filter zurücksetzen" alles mit einem Klick auf den Anfang.

Das Formular schickt seine Werte mit `method="get"`. Damit stehen Suchbegriff und Filter in der Adresse. Man kann ein Suchergebnis als Link weitergeben, und die Zurück-Taste des Browsers führt zur vorherigen Suche.

### In Antiquariat Kassius: die Checkboxen der Suche

Die Checkboxen stehen direkt in ihrer Beschriftung, dann gehört der Text ohne `for` und `id` zur Checkbox. Nach dem Absenden sind die gewählten Felder wieder angekreuzt, weil PHP bei jeder Checkbox prüft, ob ihr Wert in der Anfrage war.

```php
<div class="filter-checkboxes">
    <label><input type="checkbox" name="in[]" value="titel" <?= in_array('titel', $searchIn) ? 'checked' : '' ?>>
        Nach Titel suchen</label>
    <label><input type="checkbox" name="in[]" value="autor" <?= in_array('autor', $searchIn) ? 'checked' : '' ?>>
        Nach Autor suchen</label>
    <label><input type="checkbox" name="in[]" value="katalog" <?= in_array('katalog', $searchIn) ? 'checked' : '' ?>> Nach Katalognummer suchen</label>
    <label><input type="checkbox" name="in[]" value="kategorie" <?= in_array('kategorie', $searchIn) ? 'checked' : '' ?>> Nach Kategorie suchen</label>
</div>
```

Unter dem Kasten steht immer, wie viele Bücher gefunden wurden, und bei mehreren Seiten, auf welcher man ist. Findet die Suche nichts, erscheint „Keine Bücher gefunden." statt einer leeren Fläche. Eine leere Seite ohne Text würde aussehen, als sei etwas kaputt.

```php
<p style="color:var(--text-muted); margin-bottom:1rem; font-size:0.9rem;">
    <?= $total ?> Bücher gefunden
    <?= $totalPages > 1 ? "– Seite $page von $totalPages" : '' ?>
</p>
```

Wie aus den Checkboxen und Filtern eine sichere SQL-Abfrage wird, steht auf der [Modulseite M295](/module/m295.html#suche-und-filter-im-selben-query-bauen).

## Tabellen im Admin-Bereich

Im öffentlichen Teil sind die Bücher Karten mit Bild, weil man stöbert. Im Admin-Bereich sind sie eine Tabelle, weil man vergleicht und gezielt einen Eintrag sucht. Dieselben Daten, zwei Darstellungen, je nachdem, was die Person damit tun will.

Jede Zeile hat am Ende zwei kleine Knöpfe zum Bearbeiten und Löschen. Ein `title`-Attribut erklärt beim Darüberfahren, was sie tun. Ist die Liste leer, steht in der Tabelle eine einzelne Zeile über alle Spalten mit „Keine Bücher gefunden".

### In Antiquariat Kassius: Kopf und Zeilen

Der Tabellenkopf ist gedämpft, damit die Daten im Vordergrund stehen. Die Zeile unter der Maus wird leicht hinterlegt, so verrutscht man bei einer breiten Tabelle nicht in die Nachbarzeile.

```css
thead th {
    background: var(--muted);
    padding: 0.75rem 1rem;
    text-align: left;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--text-muted);
}

tbody td {
    padding: 0.75rem 1rem;
    border-top: 1px solid var(--muted);
    font-size: 0.92rem;
    vertical-align: middle;
}

tbody tr:hover {
    background: #faf8f3;
}
```

## Rückmeldung und Löschdialog

Jede Aktion braucht eine Antwort. Wer auf „Speichern" drückt und nichts sieht, drückt ein zweites Mal. Nach dem Speichern erscheint deshalb oben eine grüne Meldung, bei einem Fehler eine rote.

### In Antiquariat Kassius: Meldungen

Beide Meldungen stehen an derselben Stelle über dem Inhalt. Welche erscheint, hängt davon ab, ob `$success` oder `$error` gesetzt ist. Der Text läuft durch `h()`, die Funktion zum Escapen aus M295.

```php
<?php if ($success): ?>
    <div class="alert alert-success"><?= h($success) ?></div>
<?php endif; ?>
<?php if ($error): ?>
    <div class="alert alert-error"><?= h($error) ?></div>
<?php endif; ?>
```

### In Antiquariat Kassius: vor dem Löschen nachfragen

Löschen lässt sich nicht rückgängig machen. Vor dem Löschen fragt die Seite deshalb nach, und zwar mit dem Titel des Buchs, damit man sieht, welches gemeint ist. Das übernimmt `confirm()` des Browsers im `onsubmit` des Formulars: Klickt man auf „Abbrechen", gibt `confirm()` `false` zurück, und das Formular wird gar nicht erst abgeschickt.

```php
<form method="post" action="books.php"
    onsubmit="return confirm('Buch &quot;<?= h(addslashes($b['titel'] ?? '')) ?>&quot; wirklich löschen?');"
    style="display:inline;">
```

Der Titel steht mitten in einem JavaScript-Text, der selbst in einem HTML-Attribut steht. `addslashes()` sorgt dafür, dass ein Apostroph im Titel den JavaScript-Text nicht vorzeitig beendet, `h()` dafür, dass Anführungszeichen das Attribut nicht beenden.

## Barrierefreiheit

Barrierefrei heisst, dass auch Menschen die Seite benutzen können, die schlecht sehen, keine Maus benutzen oder einen Screenreader brauchen. Die Richtlinien dafür (WCAG) fassen das in vier Grundsätzen zusammen: **wahrnehmbar, bedienbar, verständlich, robust**.

Im Antiquariat umgesetzt:

- `<html lang="de">` im Header, damit ein Screenreader die Seite deutsch vorliest
- Jedes Buchcover hat den Buchtitel als Alternativtext, ein Screenreader sagt also den Titel statt „Bild"
- Checkboxen sind mit ihrer Beschriftung verbunden, einmal über `for` und `id`, einmal indem die Checkbox im `<label>` steht
- Das aktive Eingabefeld ist am blauen Rahmen erkennbar

```php
<img src="<?= BASE_URL ?>/Bilder/onwardDrakeCover.jpg" alt="<?= htmlspecialchars($book['titel']) ?>" style="width:100%; height:100%; object-fit:cover; display:block;">
```

### Kontrast, nachgerechnet

Für normalen Text verlangt die WCAG auf Stufe AA ein Kontrastverhältnis von mindestens 4.5 zu 1. Ich habe die Farbpaare aus dem Style-Guide mit der Formel der WCAG nachgerechnet:

| Farbpaar | Wo | Kontrast | AA für normalen Text |
|---|---|---|---|
| Text `#1a1a2e` auf Beige `#F5F1E8` | Fliesstext | 15.13 : 1 | erfüllt |
| Weiss auf Blau `#1a3a5c` | Header, Buttons | 11.64 : 1 | erfüllt |
| Blau `#1a3a5c` auf Beige | Überschriften | 10.33 : 1 | erfüllt |
| Weiss auf Rot `#8B1E3F` | Abzeichen „Verkauft" | 8.92 : 1 | erfüllt |
| Grau `#6b6b6b` auf Weiss | Zweitinformation auf Karten | 5.33 : 1 | erfüllt |
| Grau `#6b6b6b` auf Beige | Trefferzahl | 4.73 : 1 | erfüllt |
| Grau `#6b6b6b` auf `#e8e4d9` | Tabellenkopf | 4.19 : 1 | knapp nicht |
| Grün `#16a34a` auf Weiss | „Ja" und „Nein" in Tabellen | 3.30 : 1 | nicht |

Die beiden letzten Paare schaffen die Grenze mit einem etwas dunkleren Ton: Grün `#15803d` auf Weiss ergibt 5.02 : 1, Grau `#5f5f5f` im Tabellenkopf 5.03 : 1.

## Zusammenfassung / Spickzettel

| Thema | Kernaussage |
|---|---|
| Vorgehen | Zuerst Seitenübersicht und Masken, dann Style-Guide, erst dann Code |
| Style-Guide | Farben, Schrift, Abstände, Ecken und Komponenten einmal festlegen |
| Farben im Code | Als CSS-Variablen in `:root`, nie als Wert in einzelnen Regeln |
| Komponenten | Eine Grundklasse plus Varianten: `btn btn-primary`, `btn btn-outline` |
| Navigation | Der aktive Punkt ist markiert, öffentlicher und Admin-Bereich sehen verschieden aus |
| Formulare | Beschriftung über jedem Feld, `*` und `required` für Pflichtfelder, passender Feldtyp |
| Auswahllisten | Leere erste Option plus `required` erzwingt eine bewusste Wahl |
| Suche | `method="get"`, damit Suche und Filter in der Adresse stehen |
| Leere Ergebnisse | Immer einen Text anzeigen, nie eine leere Fläche |
| Feedback | Meldung nach jeder Aktion, Rückfrage vor dem Löschen mit dem Namen des Eintrags |
| Barrierefreiheit | `lang`, Alternativtexte, verbundene Beschriftungen, sichtbarer Fokus, Kontrast ab 4.5 : 1 |
