Fallstudie · Design-System

Design System

Die zentrale Quelle für das Interface meiner Portfolio-Website: Tokens, Typografie, Bewegung und die Komponenten, die darauf aufbauen. Alles hier ist live – Farbfelder und Beispiele nutzen dieselben CSS-Variablen wie die Website, deshalb weicht diese Seite nie vom Original ab.

Gehört zu

Portfolio-Website von Olha Bohomolova

Dieses Design-System wurde für meine eigene Website entwickelt und dokumentiert genau die Bausteine, die dort im Einsatz sind.

Zur Website
Framework
Next.js 16 · App Router
Styling
Tailwind CSS v4 · Tokens in app/globals.css
Bewegung
framer-motion 12 + CSS-Keyframes
Icons
lucide-react · 16 / 20 / 24 px

Grundlagen / 01

Farbe

Semantische Tokens als CSS-Variablen in :root, an Tailwind über @theme inline übergeben. In Komponenten nie Hex-Werte oder Klassen wie bg-white verwenden – immer den Token. Klick auf einen Wert kopiert ihn.

Marke

Primary

Marken-Violett. Akzente, Fokusring, Auswahl, der Punkt im Logo.

var
class
oklch
hex

Crayon

Handgezeichnete Striche und Betonung. Dunkler für Kontrast auf hellen Flächen.

var
class
oklch
hex

Accent

Unterstützendes Cyan. Sanfte Highlights, nie für Text.

var
class
oklch
hex

Highlight

Marker-Gelb. Sparsame Betonung hinter kurzen Phrasen.

var
class
oklch
hex

Neutraltöne

Background

Seitenhintergrund. Wird auch auf <html> gesetzt.

var
class
oklch
hex

Foreground

Fließtext, primäre Buttons, dunkle Sektionen.

var
class
oklch
hex

Card

Erhöhte Flächen: Karten, Eingabefelder, Popover.

var
class
oklch
hex

Muted

Dezente Füllungen, Hover-Zustände, Inline-Code.

var
class
oklch
hex

Muted foreground

Sekundärer Text, Labels, Bildunterschriften.

var
class
oklch
hex

Border

Haarlinien, Trenner, Kartenränder.

var
class
oklch
hex

Secondary

Warmes Off-White für abwechselnde Sektionen.

var
class
oklch
hex

Logo surface

Neutrale Fläche hinter Kundenlogos.

var
class
oklch
hex

Feedback

Destructive

Nur für Fehler und destruktive Aktionen.

var
class
oklch
hex

Success

Positive Rückmeldung: Verfügbarkeit, gespeichert, bestätigt. Nur Flächen und Punkte, nicht für Fließtext.

var
class
oklch
hex

Success foreground

Erfolgstext auf hellen Flächen, abgedunkelt für ausreichenden Kontrast.

var
class
oklch
hex

Kontrast

Jede Textfarbe gegen --background und --card im hellen Theme. WCAG AA verlangt mindestens 4.5:1 für Fließtext und 3:1 für großen Text (ab 24px oder 18.66px fett).

Textfarbeauf backgroundauf card
--foreground18.03:1AA18.84:1AA
--muted-foreground7.16:1AA7.48:1AA
--primary3.33:1nur groß3.48:1nur groß
--crayon5.91:1AA6.17:1AA
--destructive4.56:1AA4.76:1AA
--success-foreground5.37:1AA5.61:1AA

Werte werden beim Rendern aus den oklch-Tokens berechnet. Farben mit „nur groß“ dürfen nur für Überschriften, Icons oder dekorative Striche genutzt werden.

app/globals.css
:root {
  --primary: oklch(0.65 0.18 300);
  --crayon: oklch(0.52 0.22 300);
}

@theme inline {
  --color-primary: var(--primary);
  --color-crayon: var(--crayon);
}

/* usage */
<span className="text-primary" />

Grundlagen / 02

Typografie

Zwei Schriftfamilien. Syne trägt die Persönlichkeit in Überschriften, Space Grotesk hält den Fließtext ruhig und technisch. Fließtext ist nie kleiner als 14px.

Aa

Syne

Display & Überschriften · Schnitte 600 · 700

--font-displayfont-[family-name:var(--font-display)]

Aa

Space Grotesk

Fließtext & UI · Schnitte 400 · 500 · 600

--font-sansfont-sans

Display

Syne 700 · clamp(2.5rem, 8vw, 5rem)

Olha.

Heading 1

Syne 700 · clamp(2.5rem, 8vw, 5rem)

Ausgewählte Arbeiten

Heading 2

Syne 700 · clamp(2rem, 5vw, 3rem)

SmartDock Fallstudie

Heading 3

Syne 700 · text-xl md:text-2xl

Die Entscheidung

Body large

Space Grotesk 400 · text-lg

Ich gestalte Interfaces, die sich schon beim ersten Mal selbstverständlich anfühlen.

Body

Space Grotesk 400 · text-base

Research, Flows und Prototypen – von der ersten Skizze bis zur Übergabe an die Entwicklung.

Small

Space Grotesk 400 · text-sm · muted

Bildunterschriften, Meta-Informationen und Hilfetexte.

Eyebrow

Space Grotesk 500 · text-xs · uppercase

UI/UX · Fallstudie

Responsive Skala

Exakte Schriftgrößen pro Breakpoint in px, gemessen bei der kleinsten Breite des Breakpoints (base = 375px). Zeilenhöhe in px, Laufweite in em.

StilbasesmmdlgxlZeilenhöheLaufweite
Display40px51px61px80px80px1.1 → 44 / 56 / 67 / 88 / 88px-0.02em
Heading 140px51px61px80px80px0.95 → 38 / 49 / 58 / 76 / 76px-0.02em
Heading 232px32px38px48px48px0.95 → 30 / 30 / 36 / 46 / 46px-0.02em
Heading 320px20px24px24px24px28 / 28 / 32 / 32 / 32px0em
Body large18px18px18px18px18px29.25px (1.625)0em
Body16px16px16px16px16px26px (1.625)0em
Small14px14px14px14px14px22.75px (1.625)0em
Eyebrow12px12px12px12px12px16px0.2em (2.4px)

Grundlagen / 03

Abstände & Layout

Ein 4px-Grundraster über die Tailwind-Skala. Für Abstände zwischen Geschwistern gap-* verwenden – nie Margins mit gap mischen und nie space-*-Utilities.

14px
28px
312px
416px
624px
832px
1248px
1664px
2496px
32128px

Container

max-w-6xl mx-auto

1152px Inhaltsbreite auf allen Seiten.

Seitenränder

px-4 sm:px-6 md:px-12

16 → 24 → 48px horizontales Padding.

Sektionsrhythmus

py-24 md:py-32

96 → 128px zwischen großen Sektionen.

Breakpoints · Mobile first

sm ≥ 640pxmd ≥ 768pxlg ≥ 1024pxxl ≥ 1280px

Grundlagen / 04

Radius

Abgeleitet von einem einzigen --radius: 1rem. Regel: Alles, was eine sichtbare Form hat und klickbar ist (Buttons, Pills, Eingabefelder, Icon-Buttons), ist rounded-full. Textlinks wie die Hauptnavigation haben keine Form und damit keinen Radius. Einzige Ausnahme: Zeilen in vertikalen Listen (Seitenleiste dieser Seite, Dropdown-Einträge) nutzen rounded-md, weil volle Pills dort wie gestapelte Buttons wirken. Container nutzen lg.

rounded-sm

12px · Tags, kleine Chips

rounded-md

14px · Zeilen in vertikalen Listen (Ausnahme)

rounded-lg

16px · Karten, Medien

rounded-xl

20px · Große Panels

rounded-full

9999px · Buttons, Pills, Eingabefelder

Grundlagen / 05

Tiefe

Flächen sind im Ruhezustand flach. Tiefe entsteht nur als Feedback beim Hover – und ist immer violett statt grau getönt. Fahr mit der Maus über die Karten.

Flat

Standard. Nur Rahmen.

hover-lift

Karten. −8px + violetter Schatten, Feder-Easing.

hover-glow

Primäre CTAs. Weiches violettes Leuchten.

Grundlagen / 06

Bewegung

Bewegung erklärt, sie dekoriert nicht. Ein gestaffelter Einstieg pro Sektion, kurzes Hover-Feedback, nichts, was hinter Inhalten in Schleife läuft. Bei prefers-reduced-motion ist alles deaktiviert. Fahr über eine Zeile, um die Kurve zu sehen.

ease-out-expo

cubic-bezier(0.22, 1, 0.36, 1)

Einstiege, Reveals, underline-slide

spring-back

cubic-bezier(0.34, 1.56, 0.64, 1)

hover-lift, magnetische Buttons

in-out

cubic-bezier(0.65, 0, 0.35, 1)

Zeichnen des Stiftstrichs

linear

linear

Nur Laufband

Dauer

200ms

Farbe und Deckkraft beim Hover

300ms

Unterstreichungen, Rahmen, Buttons

600ms

Einstieg von Sektionen und Karten

1100ms

Einzeichnen des Stiftstrichs

framer-motion · Sektions-Einstieg
<motion.div
  initial={{ opacity: 0, y: 20 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true }}
  transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: index * 0.1 }}
/>

Komponenten

Komponenten

Aufgebaut aus den Tokens oben. Klassen-Strings unverändert übernehmen – das sind die Produktionswerte.

Button

components/portfolio/hero.tsx

Nur zwei Varianten. Primär = einmal pro Ansicht. Icon rechts, verschiebt sich beim Hover um 4px. Höhe 44px mobil, 56px ab sm (sekundär +4px Rahmen).

Zustände

Hover, Fokus und Gedrückt sind hier statisch erzwungen, damit alle Zustände nebeneinander sichtbar sind.

  • Standard
  • Hover
  • Fokus
  • Gedrückt
  • Deaktiviert
  • Lädt
  • Standard
  • Hover
  • Fokus
  • Gedrückt
  • Deaktiviert
  • Lädt

Spezifikation

Varianten
primary (gefüllt), secondary (Rahmen). Beide mit optionalem Icon rechts.
Größen
Eine Größe, responsive
Höhe
44px base · 56px sm+ (secondary + 2 × 2px border)
Innenabstand
px-6 py-3 → sm:px-8 sm:py-4 (24 × 12 → 32 × 16px)
Radius-Token
--radius-full · rounded-full
Farb-Tokens
--foreground, --background, --primary (glow, hover), --border (foreground/20)
Bewegung
all 300ms · magnetic 300ms cubic-bezier(0.34, 1.56, 0.64, 1) · glow 400ms ease
Barrierefreiheit
Native <button> oder <a>. Lädt: aria-busy="true" und disabled, Text bleibt sichtbar. Ziel mind. 44 × 44px.
tsx
// primary
<a className="group inline-flex items-center justify-center gap-2 sm:gap-3 px-6 sm:px-8 py-3 sm:py-4 rounded-full font-medium transition-all duration-300 text-sm sm:text-base disabled:pointer-events-none disabled:opacity-50 bg-foreground text-background hover:shadow-2xl hover:shadow-primary/20 hover-glow magnetic-hover">…</a>

// secondary
<a className="group inline-flex items-center justify-center gap-2 sm:gap-3 px-6 sm:px-8 py-3 sm:py-4 rounded-full font-medium transition-all duration-300 text-sm sm:text-base disabled:pointer-events-none disabled:opacity-50 bg-transparent border-2 border-foreground/20 text-foreground hover:border-primary hover:bg-primary/5 magnetic-hover">…</a>

// loading
<button disabled aria-busy="true">Wird gesendet <Loader2 className="motion-safe:animate-spin" /></button>

Navigationslink

components/portfolio/navigation.tsx

Gedämpft im Ruhezustand, Vordergrundfarbe beim Hover, Verlaufs-Unterstrich fährt aus der Mitte aus. Alle Menüpunkte nutzen exakt diese Klasse.

Zustände

Hover, Fokus und Gedrückt sind hier statisch erzwungen, damit alle Zustände nebeneinander sichtbar sind.

Spezifikation

Varianten
Standard, aktuelle Seite (aria-current="page").
Größen
Eine Größe
Höhe
36px (py-2 + 20px line)
Innenabstand
px-4 py-2 (16 × 8px)
Radius-Token
— (text link, no shape)
Farb-Tokens
--muted-foreground → --foreground, --primary (underline)
Bewegung
colour 150ms · underline width 300ms cubic-bezier(0.22, 1, 0.36, 1)
Barrierefreiheit
In <nav aria-label> mit <a>. Fokus über globalen Outline-Stil. Klickfläche 40px hoch, mobil im Menü 48px.
tsx
<a className="relative px-4 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground underline-slide" aria-current="page" />

Status-Pill

Verfügbarkeit oder Meta-Informationen. Primärfarbe mit 10% Füllung, 20% Rahmen. Der Live-Punkt nutzt den Token --success (bg-success).

Offen für neue Projekte

Zustände

Hover, Fokus und Gedrückt sind hier statisch erzwungen, damit alle Zustände nebeneinander sichtbar sind.

  • Standard
    Offen für neue Projekte
  • Hover
    Nicht vorhandenNicht interaktiv: keine Hover-, Fokus- oder Gedrückt-Zustände.
  • Fokus
    Nicht vorhandenNicht interaktiv: keine Hover-, Fokus- oder Gedrückt-Zustände.
  • Gedrückt
    Nicht vorhandenNicht interaktiv: keine Hover-, Fokus- oder Gedrückt-Zustände.
  • Deaktiviert
    Nicht vorhandenNicht interaktiv: keine Hover-, Fokus- oder Gedrückt-Zustände.

Spezifikation

Varianten
Verfügbar (Live-Punkt). Nur ein Einsatz pro Seite.
Größen
Eine Größe
Höhe
36px
Innenabstand
px-4 py-2 (16 × 8px) · gap-2
Radius-Token
--radius-full · rounded-full
Farb-Tokens
--primary (/10 fill, /20 border, text), --success (dot)
Bewegung
ping 1s cubic-bezier(0, 0, 0.2, 1), motion-safe only
Barrierefreiheit
Reiner Text, keine Rolle. Der Punkt ist dekorativ (aria-hidden). Ping-Animation nur mit motion-safe.
tsx
<span className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/10 px-4 py-2">
  <span className="relative flex size-2" aria-hidden="true">
    <span className="absolute size-full rounded-full bg-success opacity-75 motion-safe:animate-ping" />
    <span className="relative size-2 rounded-full bg-success" />
  </span>
  <span className="text-sm font-medium text-primary">…</span>
</span>

Stiftstrich

components/portfolio/crayon-stroke.tsx

Das Signature-Element. Handgezeichnetes SVG mit Körnungsfilter. Maximal einer pro Viewport, auf einem einzigen Schlüsselwort. Modi: loop | inView.

Design, das wirkt

tsx
import { CrayonStroke } from "@/components/portfolio/crayon-stroke"

<span className="relative inline-block">
  works
  <CrayonStroke shape="loop" mode="inView" className="absolute -inset-x-4 -inset-y-3" />
</span>

Projektkarte

components/portfolio/works.tsx

Erst das Bild, dann Eyebrow, Titel, einzeilige Zusammenfassung. Die ganze Karte ist der Link. Hebt sich beim Hover.

Zustände

Hover, Fokus und Gedrückt sind hier statisch erzwungen, damit alle Zustände nebeneinander sichtbar sind.

Spezifikation

Varianten
Projekt mit Bild, Eyebrow, Titel und Kurzbeschreibung.
Größen
Füllt die Rasterspalte
Höhe
auto · image 4:3
Innenabstand
p-5 (20px) · gap-2
Radius-Token
--radius-lg · rounded-lg (16px)
Farb-Tokens
--card, --border, --foreground, --muted-foreground, --primary (shadow)
Bewegung
lift −8px 400ms cubic-bezier(0.34, 1.56, 0.64, 1) · shadow 400ms ease
Barrierefreiheit
Die ganze Karte ist ein einziges <a>, Titel als Linktext. Bild mit Alt-Text, Pfeil-Icon aria-hidden.
tsx
<a className="hover-lift group flex w-full max-w-sm flex-col overflow-hidden rounded-lg border border-border bg-card">
  <Image … />
  <div className="flex flex-col gap-2 p-5">…</div>
</a>

Eingabefeld

Neue Komponente, nur aus vorhandenen Tokens gebaut: --card, --border, --primary, --destructive und rounded-full. Label immer sichtbar über dem Feld, Platzhalter nur als Beispiel.

Ich antworte innerhalb von zwei Werktagen.

Zustände

Hover, Fokus und Gedrückt sind hier statisch erzwungen, damit alle Zustände nebeneinander sichtbar sind.

  • Standard

    Ich antworte innerhalb von zwei Werktagen.

  • Hover

    Ich antworte innerhalb von zwei Werktagen.

  • Fokus

    Ich antworte innerhalb von zwei Werktagen.

  • Ausgefüllt

    Ich antworte innerhalb von zwei Werktagen.

  • Fehler

    Bitte gib eine gültige E-Mail-Adresse ein.

  • Deaktiviert

    Ich antworte innerhalb von zwei Werktagen.

Spezifikation

Varianten
Standard, mit Hilfetext, mit Fehlermeldung.
Größen
Eine Größe, volle Breite
Höhe
48px (h-12)
Innenabstand
px-5 (20px) · label/help gap-2
Radius-Token
--radius-full · rounded-full
Farb-Tokens
--card, --border, --foreground, --muted-foreground, --primary (focus), --destructive (error)
Bewegung
colours 300ms ease · no movement
Barrierefreiheit
Immer sichtbares <label for>. Hilfe- und Fehlertext per aria-describedby, Fehler mit aria-invalid. Höhe 48px.
tsx
<label htmlFor="email">E-Mail</label>
<input
  id="email"
  type="email"
  aria-invalid={hasError}
  aria-describedby="email-msg"
  className="h-12 w-full rounded-full border border-border bg-card px-5 text-sm text-foreground placeholder:text-muted-foreground transition-colors duration-300 hover:border-foreground/30 focus-visible:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:ring-destructive/30"
/>
<p id="email-msg" className="text-sm text-muted-foreground">…</p>

shadcn/ui-Primitives liegen in components/ui und erben dieselben Tokens.

Richtlinien / 01

Barrierefreiheit

Grundanforderungen für jede neue Seite und Komponente.

Fokus
2px Outline in Primärfarbe, 2px Abstand bei :focus-visible. Niemals entfernen.
Reduzierte Bewegung
prefers-reduced-motion deaktiviert global alle Animationen; der Stiftstrich wird vollständig gezeichnet angezeigt.
Kontrast
Fließtext auf Hintergrund ≥ 4.5:1. Für violetten Text auf hellen Flächen crayon statt primary verwenden.
Schriftgröße
Mindestens 14px. Zeilenhöhe im Fließtext 1.5–1.6 (leading-relaxed).
Semantik
Ein h1 pro Seite, Landmarks (header, main, nav), Alt-Text für jedes inhaltliche Bild, sr-only-Labels für Icon-Buttons.
Touch
Interaktive Flächen auf Mobilgeräten ≥ 44px hoch.
  • Semantische Tokens verwenden: bg-card, text-muted-foreground.
  • Farben hart codieren: bg-white, text-[#333], text-gray-500.
  • Ein gemeinsames Markup für alle Sprachen – nur der Text wird getauscht.
  • Sprachspezifisches JSX mit unterschiedlichem Styling schreiben.
  • Ein Stiftstrich pro Viewport, auf dem Schlüsselwort.
  • Dekorative Blobs, Leuchtkugeln oder Schleifen hinter Lesetext.

Richtlinien / 02

Inhalte & Sprachen

Deutsch ist die Quelle. Jede Änderung in de wird im selben Commit in en und uk übernommen – mit identischen Keys und Array-Längen.

de · Quelleenuk
  • lib/translations.ts
  • components/portfolio/hero.tsx → HERO_INTRO
  • app/projects/*/page.tsx → content
  • lib/graphic-design-content.ts → GRAPHIC_CONTENT
  • app/impressum/page.tsx → content

Zahlen werden immer als Ziffern geschrieben (6 Jahre, nicht sechs Jahre). Ukrainische Texte vor der Veröffentlichung auf fehlerhaftes UTF-8 prüfen.

Richtlinien

Developer Handoff

So geht das System an die Entwicklung: dieselben Namen in Figma und im Code, eine feste Checkliste pro Übergabe und klare Dateiorte.

Token-Benennung

Figma Variables tragen dieselben Namen wie die CSS-Variablen, nur mit Gruppe und Schrägstrich. Wer in Figma eine Farbe sieht, findet sie im Code ohne Übersetzung.

Figma VariableCSS-VariableTailwind-Klasse
color/primary--primarybg-primary · text-primary
color/crayon--crayontext-crayon
color/accent--accentbg-accent
color/highlight--highlightbg-highlight
color/background--backgroundbg-background
color/foreground--foregroundtext-foreground
color/card--cardbg-card
color/muted-foreground--muted-foregroundtext-muted-foreground
color/border--borderborder-border
color/success--successbg-success
color/destructive--destructivetext-destructive
radius/lg--radius-lgrounded-lg
font/display--font-displayfont-[family-name:var(--font-display)]
font/sans--font-sansfont-sans

Jede Übergabe enthält

  • Alle Zustände

    Standard, Hover, Fokus, Gedrückt, Deaktiviert und Lädt für jede interaktive Komponente.

  • Responsives Verhalten

    Layout und Schriftgrößen für base, sm, md, lg und xl, inklusive Umbrüchen.

  • Grenzfälle

    Sehr langer Text, leerer Zustand, Fehlerzustand und fehlende Bilder.

  • Bewegung in Zahlen

    Dauer in ms und Easing-Kurve als cubic-bezier, nicht „etwas federnd“.

  • Exportierte Assets

    SVGs optimiert, Lottie als JSON mit Notiz, wann die Animation startet, ob sie wiederholt und was bei reduzierter Bewegung passiert.

  • Barrierefreiheit

    Kontrastwerte, Fokus-Reihenfolge, ARIA-Rollen, Alt-Texte und Zielgrößen ab 44px.

Dateiorte im Code

app/globals.css
Alle Tokens (:root, .dark), @theme-Mapping, Utilities wie hover-lift.
components/portfolio/*
Komponenten der Live-Seite: Navigation, Hero, Projekte, Kontakt.
components/ui/*
shadcn/ui-Basiskomponenten, über Tokens gestylt.
components/design-system/*
Diese Dokumentation.

Richtlinien

Changelog

Versionen dieses Systems. Jede Änderung an Tokens oder Komponenten bekommt einen Eintrag.

  1. v1.1

    • Token --success (und --success-foreground) ergänzt, Live-Punkt nutzt ihn statt fest codiertem Grün.
    • --highlight über @theme gemappt, nutzbar als bg-highlight.
    • Hex-Werte werden beim Rendern berechnet, Kontrastwerte für alle Textfarben.
    • Radius-Regel geklärt: Ausnahme für Zeilen in vertikalen Listen benannt.
    • Responsive Schriftskala mit exakten Werten pro Breakpoint.
    • Alle Zustände und Spezifikationen für jede Komponente, neues Eingabefeld.
    • Neue Abschnitte Developer Handoff und Changelog.
  2. v1.0

    • Erste Version: Farben, Typografie, Abstände, Radius, Elevation, Bewegung, Komponenten und Richtlinien.