Designsystem
Die Designsprache, aus der ranui gebaut ist, und der vollständige Katalog der Tokens, die sie ausdrücken: jede globale --ran-*-Custom-Property, die die Bibliothek deklariert, samt ihrem Wert in beiden Themes. Komponenten lesen diese Tokens, statt Werte festzuschreiben — ein Token zu überschreiben gestaltet also alles um, was es verwendet.
Drei Seiten beantworten drei verschiedene Fragen, und sie sind bewusst getrennt:
| Seite | Beantwortet |
|---|---|
| Designsystem (diese Seite) | Was die Tokens sind: das Vokabular |
| Gestaltungsleitlinien | Wie man wählt, wenn man eine Oberfläche baut |
| Themengestaltung | Wie man sie zur Laufzeit umschaltet und überschreibt |
Einsetzen, wenn du den Namen oder den Wert eines Tokens brauchst (eine Farbrolle, eine Abstandsstufe, eine Symbolgröße, eine Schattenstufe, eine Beschleunigungskurve) oder verstehen willst, warum die Skalen so geformt sind, wie sie sind.
Die Sprache: Geist
Die Tokens von ranui beruhen auf Geist, dem quelloffenen Designsystem von Vercel. Jede Farbskala ist eine Leiter fester Aufgaben, eine je Sprosse, kein Vorrat an Tönen zur Auswahl: Sprosse 200 ist nicht „ein etwas dunkleres Grau“, sie ist „der Hintergrund beim Überfahren“. Steht die Aufgabe einer Sprosse fest, ist die Farbwahl für einen Interaktionszustand ein Nachschlagen, keine Ermessensfrage.
ranui übernimmt diese Leiter als seine --ran-*-Skalen, legt semantische Tokens darüber und liefert Geist Sans / Geist Mono als Standardschriften mit.
Zwei Ebenen
Ebene 1: die Basispalette. Die rohen Skalen weiter unten. Selten unmittelbar verwendet.
Ebene 2: die semantischen Tokens. --ran-color-* und Verwandte, auf Ebene 1 abgebildet. Verwende diese Ebene. Der Dunkelmodus definiert nur Ebene 1 neu, jedes semantische Token wechselt also über var() mit — ohne eine einzige Dunkel-Sonderregel je Komponente irgendwo in der Bibliothek.
--ran-gray-1000 → #171717 (hell) / #ededed (dunkel) ← Ebene 1, wechselt
--ran-color-text → var(--ran-gray-1000) ← Ebene 2, folgt
--ran-btn-color → var(--ran-color-text, …) ← Komponenten-TokenDiese Kette ist die ganze Architektur: Ändere eine Basissprosse, und es wirkt überall; ändere ein semantisches Token, und es ändert eine Rolle; ändere ein Komponenten-Token, und es ändert ein Element.
Farbe
Die Leiter
Jede Farbtonskala läuft von 100 bis 1000, und jede Sprosse hat genau eine Aufgabe:
| Sprosse | Rolle | Sprosse | Rolle |
|---|---|---|---|
| 100 | Standardhintergrund | 600 | Rahmen im gedrückten Zustand |
| 200 | Hintergrund beim Überfahren | 700 | Deckende Füllung (Schaltfläche/Abzeichen) |
| 300 | Hintergrund beim Drücken | 800 | Deckende Füllung (Überfahren) |
| 400 | Standardrahmen | 900 | Sekundärer Text und Symbole |
| 500 | Rahmen beim Überfahren | 1000 | Primärer Text und Symbole |
Hintergründe
| Token | Hell | Dunkel | Wofür |
|---|---|---|---|
--ran-background-100 | #ffffff | #000000 | Seitenhintergrund |
--ran-background-200 | #fafafa | #000000 | Dezente Zonen der Seite |
Grau — --ran-gray-100..1000
Die Skala hinter Text, Rahmen und Flächen.
| Sprosse | Hell | Dunkel |
|---|---|---|
| 100 | #f2f2f2 | #1a1a1a |
| 200 | #ebebeb | #1f1f1f |
| 300 | #e6e6e6 | #292929 |
| 400 | #eaeaea | #2e2e2e |
| 500 | #c9c9c9 | #454545 |
| 600 | #a8a8a8 | #878787 |
| 700 | #8f8f8f | #8f8f8f |
| 800 | #7d7d7d | #7d7d7d |
| 900 | #4d4d4d | #a0a0a0 |
| 1000 | #171717 | #ededed |
Grau mit Alpha — --ran-gray-alpha-100..1000
Durchscheinend, legt sich also über jede Fläche: die richtige Wahl für einen Schleier, einen Hover-Hauch oder eine Trennlinie, die auf unbekanntem Inhalt liegen muss.
| Sprosse | Hell | Dunkel |
|---|---|---|
| 100 | #0000000d | #ffffff12 |
| 200 | #00000015 | #ffffff17 |
| 300 | #0000001a | #ffffff21 |
| 400 | #00000014 | #ffffff24 |
| 500 | #00000036 | #ffffff3d |
| 600 | #0000003d | #ffffff82 |
| 700 | #00000070 | #ffffff8a |
| 800 | #00000082 | #ffffff78 |
| 900 | #000000b3 | #ffffff9c |
| 1000 | #000000e8 | #ffffffeb |
Blau — --ran-blue-100..1000
Reserviert für Links und den Fokusring.
| Sprosse | Hell | Dunkel |
|---|---|---|
| 100 | #f0f7ff | #06193a |
| 200 | #e9f4ff | #022248 |
| 300 | #dfefff | #002f62 |
| 400 | #cae7ff | #003674 |
| 500 | #94ccff | #00418b |
| 600 | #48aeff | #0090ff |
| 700 | #006bff | #006efe |
| 800 | #0059ec | #005be7 |
| 900 | #005ff2 | #47a8ff |
| 1000 | #002359 | #eaf6ff |
Rot — --ran-red-100..1000
Gefahr und Fehler.
| Sprosse | Hell | Dunkel |
|---|---|---|
| 100 | #ffeeef | #330a11 |
| 200 | #ffe8ea | #440d13 |
| 300 | #ffe3e4 | #5d0e17 |
| 400 | #ffd7d6 | #6f101b |
| 500 | #ffb1b3 | #88151f |
| 600 | #ff676d | #f32e40 |
| 700 | #fc0035 | #f13242 |
| 800 | #ea001d | #e2162a |
| 900 | #d8001b | #ff565f |
| 1000 | #47000c | #ffe9ed |
Bernstein — --ran-amber-100..1000
Warnungen.
| Sprosse | Hell | Dunkel |
|---|---|---|
| 100 | #fff6de | #2a1700 |
| 200 | #fff4cf | #361900 |
| 300 | #fff1c1 | #502800 |
| 400 | #ffdc73 | #5b3000 |
| 500 | #ffc543 | #703e00 |
| 600 | #ffa600 | #ed9a00 |
| 700 | #ffae00 | #ffae00 |
| 800 | #ff9300 | #ff9300 |
| 900 | #aa4d00 | #ff9300 |
| 1000 | #561900 | #fff3d5 |
Grün — --ran-green-100..1000
Erfolg.
| Sprosse | Hell | Dunkel |
|---|---|---|
| 100 | #ecfdec | #002608 |
| 200 | #e5fce7 | #00320b |
| 300 | #d3fad1 | #003a0e |
| 400 | #b9f5bc | #004615 |
| 500 | #82eb8d | #006717 |
| 600 | #4ce15e | #00952d |
| 700 | #28a948 | #00ac3a |
| 800 | #279141 | #009432 |
| 900 | #107d32 | #00ca50 |
| 1000 | #003a00 | #d8ffe4 |
Semantische Farbtokens
Die Ebene, die Komponenten tatsächlich lesen. Alles hier löst sich über die Skalen oben auf und wechselt daher von selbst mit dem Theme.
| Token | Löst auf zu | Rolle |
|---|---|---|
--ran-color-bg | --ran-background-100 | Seitenhintergrund |
--ran-color-bg-subtle | --ran-background-200 | Dezente Zonen der Seite |
--ran-color-bg-elevated | --ran-background-100 · gray-100 (dunkel) | Karten, Flächen |
--ran-color-bg-muted | --ran-gray-100 | Eingesenkte / gedämpfte Füllungen |
--ran-color-bg-hover | --ran-gray-200 | Fläche beim Überfahren |
--ran-color-bg-active | --ran-gray-300 | Fläche beim Drücken |
--ran-color-text | --ran-gray-1000 | Haupttext |
--ran-color-text-secondary | --ran-gray-900 | Sekundärer Text |
--ran-color-text-disabled | --ran-gray-700 | Deaktivierter Text |
--ran-color-border | --ran-gray-400 | Standardrahmen |
--ran-color-border-secondary | --ran-gray-300 | Dezenterer Rahmen |
--ran-color-border-hover | --ran-gray-500 | Rahmen beim Überfahren |
--ran-color-border-active | --ran-gray-600 | Rahmen beim Drücken |
--ran-color-primary | --ran-gray-1000 | Die Hauptaktion (monochrom) |
--ran-color-primary-hover | #383838 · #cccccc (dunkel) | Primär beim Überfahren |
--ran-color-primary-active | #4d4d4d · #b3b3b3 (dunkel) | Primär beim Drücken |
--ran-color-primary-text | --ran-background-100 | Die Tinte auf einer primären Fläche |
--ran-color-success | --ran-green-700 | Erfolg |
--ran-color-warning | --ran-amber-700 | Warnung |
--ran-color-danger | --ran-red-700 | Gefahr / Fehler |
--ran-color-link | --ran-blue-700 | Links |
--ran-color-primary-hover / -active sind die beiden Literale der semantischen Ebene: Sie bewegen sich auf den Seitenhintergrund zu statt entlang einer Skala, deshalb definiert der Dunkelmodus sie unmittelbar neu.
Was jeder Akzent bedeutet
- Primär ist monochrom: schwarz auf weiß im Hellen, weiß auf schwarz im Dunklen (der Markenton von Geist,
<r-button type="primary">). Text und Symbole darauf nutzen--ran-color-primary-text, das mitwechselt. Ein eigenes „Kontrast“-Token gibt es nicht: Primär ist die Aktion mit dem höchsten Kontrast. - Blau ist reserviert für Links (
--ran-color-link) und den Fokusring. Es ist kein alternatives Primär. - Grün = Erfolg · Bernstein = Warnung · Rot = Gefahr. Je eine Bedeutung.
Es gibt kein --ran-color-error; das Token heißt --ran-color-danger. Ein var(), das eine nie deklarierte Eigenschaft nennt, löst sich zu nichts auf, und die ganze Deklaration fällt stillschweigend weg — deshalb lohnt es, einen falschen Namen an dieser Tabelle zu prüfen statt zu raten.
Abstand
Die Zwischenräume zwischen Dingen: padding, margin, gap. Eine Basiseinheit von 4px mit neun Werten, mehr nicht:
| Token | Wert | Token | Wert |
|---|---|---|---|
--ran-space-1 | 4px | --ran-space-8 | 32px |
--ran-space-2 | 8px | --ran-space-10 | 40px |
--ran-space-3 | 12px | --ran-space-16 | 64px |
--ran-space-4 | 16px | --ran-space-24 | 96px |
--ran-space-6 | 24px |
Die Zahl ist das Vielfache von 4px, die Skala springt also: Ein --ran-space-5 gibt es nicht. Genau darum geht es: Eine begrenzte Auswahl erzeugt den Rhythmus einer Seite.
Größen
Die eigenen Maße eines Elements: Symbolgrößen, Höhen von Bedienelementen, kleine quadratische oder rechteckige Steuerelemente.
| Token | Wert | Typischerweise |
|---|---|---|
--ran-size-1 | 16px | Kästchen einer Checkbox, kleines Symbol im Fließtext |
--ran-size-2 | 18px | — |
--ran-size-3 | 20px | Symbol innerhalb eines Bedienelements |
--ran-size-4 | 24px | Symbolschaltfläche in einer Werkzeugleiste |
--ran-size-5 | 28px | Höhe eines kompakten Bedienelements |
--ran-size-6 | 30px | — |
--ran-size-7 | 32px | Standardhöhe eines Bedienelements |
Das ist mit Absicht eine eigene Skala neben dem Abstand, und beide zu mischen ist ein maschinell geprüfter Fehler (sizing-scale). Die beiden haben unterschiedliche Bereiche und Abstufungen (eine Abstandsskala, die ab 4px verdoppelt, ergibt für Symbol- und Bedienelementgrößen ungeschickte Werte), und wer sie verwendet, muss die eine nachjustieren können, ohne die andere zu stören: Ein größer werdendes Symbol soll nicht zugleich jeden Zwischenraum verbreitern, der zufällig denselben Pixelwert teilt. Wo eine Sprosse zahlenmäßig mit einer Abstandsstufe zusammenfällt (--ran-size-4 und --ran-space-6 sind beide 24px), ist das Zufall, kein Alias.
Ein wirklich einmaliges Maß, das keine andere Komponente teilt (etwa das min-width eines Menüs), bleibt ein schlichtes Komponenten-Token mit eigenem literalen Rückfallwert, statt in eine Stufe gezwungen zu werden.
Typografie
| Token | Wert |
|---|---|
--ran-font-family | Geist / Geist Sans, danach der System-UI-Stapel |
--ran-font-mono | Geist Mono, danach ui-monospace, SF Mono, Menlo, Consolas, … |
--ran-font-size | 14px (die Basisgröße) |
--ran-line-height | 1.5715 |
Schrift ist nach Rolle geordnet, und die Rolle legt Schriftart, Größe, Stärke und Zeilenhöhe gemeinsam fest:
| Rolle | Verwendung | Stärke-Token | Größen-Tokens |
|---|---|---|---|
| heading | Überschriften | --ran-text-heading-weight (600) | --ran-text-heading-1..4 (32/24/20/16px) |
| label | Einzeilig, zum Überfliegen | --ran-text-label-weight (500) | --ran-text-label-1..3 (14/13/12px) |
| copy | Mehrzeiliger Fließtext | --ran-text-copy-weight (400) | --ran-text-copy-1..2 (16/14px) |
| button | Text auf Schaltflächen | --ran-text-button-weight (500) | --ran-text-button-size (14px) |
| mono | Code, Daten, Dachzeilen | --ran-text-mono-weight-regular (400) / --ran-text-mono-weight-medium (500) | übernimmt die Größen von label / copy |
Zwei Tokens gibt es nur, damit eine Rolle richtig sitzt:
| Token | Wert | Warum |
|---|---|---|
--ran-text-heading-tracking | -0.03em | Überschriften brauchen in großen Graden eine engere Laufweite. |
--ran-text-button-line-height | 1 | Sauberes vertikales Zentrieren in einem Bedienelement mit fester Höhe. |
Geist deckelt die Stärke bei 600 (Semibold). Betonung kommt aus Größe und Abstand, nicht aus einem fetteren Schnitt. Ein --ran-text-copy-3 gibt es nicht: Die 12px-Stufe heißt --ran-text-label-3.
Schriften
ranui hostet beide Schnitte selbst (variable Stärke 100–900, SIL OFL 1.1), ein einziger Import lädt sie also ohne CDN-Abhängigkeit:
import 'ranui/fonts'; // Bundler<link rel="stylesheet" href="…/ranui/dist/fonts/fonts.css" />Ohne ihn fallen die Tokens auf die Schriftstapel des Systems zurück; alles funktioniert weiter, nur ohne die Geist-Schnitte.
Radius
| Token | Wert | Wofür |
|---|---|---|
--ran-radius-sm | 6px | Bedienelemente: Schaltfläche, Eingabefeld, Auswahl |
--ran-radius-md | 12px | Karten, Dialoge |
--ran-radius-lg | 16px | Große Flächen |
--ran-radius-full | 9999px | Pillenformen, Profilbilder |
Elevation
Schatten ist eine Rolle, keine Dekoration. Wähle die Stufe danach, was das Element ist. Der Dunkelmodus ersetzt alle drei, denn ein für eine weiße Seite abgestimmter Schatten verschwindet auf einer schwarzen.
| Token | Wofür | Hell | Dunkel |
|---|---|---|---|
--ran-shadow-elevated | Flächen im Fluss, die zusätzlich einen Rahmen haben: r-card, r-section | 0 1px 2px rgba(0,0,0,.04), 0 2px 4px -2px rgba(0,0,0,.05) | 0 1px 2px rgba(0,0,0,.16) |
--ran-shadow-menu | Vorübergehende Ebenen über dem Inhalt: Auswahlliste, Select-Menü, Popover, Hinweis | 0 2px 4px rgba(0,0,0,.05), 0 8px 24px -6px rgba(0,0,0,.14) | 0 1px 1px rgba(0,0,0,.2), 0 4px 8px -4px rgba(0,0,0,.4), 0 16px 24px -8px rgba(0,0,0,.5) |
--ran-shadow-modal | Blockierende Dialoge: r-modal | 0 4px 12px rgba(0,0,0,.08), 0 20px 48px -12px rgba(0,0,0,.22) | 0 1px 1px rgba(0,0,0,.2), 0 8px 16px -4px rgba(0,0,0,.4), 0 24px 32px -8px rgba(0,0,0,.5) |
Rahmenlose Overlays verlassen sich für die Abgrenzung allein auf den Schatten, die Overlay-Stufen tragen also echtes Gewicht; ein Overlay, das auf die gehobene Stufe zurückfällt, wirkt flach und an die Seite geheftet.
Stapelung
Schwebende Overlays werden nach <body> portalt und brauchen daher eine ausdrückliche Stufe:
| Token | Standard | Wofür |
|---|---|---|
--ran-z-modal | 1000 | Blockierende Dialoge und ihre Maske |
--ran-z-dropdown | 1100 | Auswahlliste / Select-Menü / Popover: über dem Modal, damit ein Select in einem Dialog sichtbar bleibt |
--ran-z-message | 1200 | Hinweise und Benachrichtigungen: immer obenauf |
Die Leiter beginnt bei 1000, damit sie gewöhnliches Seitenrahmenwerk überragt (Navigationsleisten und Hintergründe liegen üblicherweise im Zehnerbereich). Überschreibe eine Stufe an :root oder je Komponente (--ran-dropdown-host-z-index, --ran-modal-root-z-index, --ran-message-z-index), niemals mit !important.
Bewegung
| Token | Wert | Verwendung |
|---|---|---|
--ran-motion-duration-fast | 0.15s | Übergänge beim Überfahren und Drücken |
--ran-motion-duration-base | 0.2s | Popovers, Menüs |
--ran-motion-duration-slow | 0.35s | Größere Einblendungen |
| Beschleunigungs-Token | Kurve | Charakter |
|---|---|---|
--ran-motion-ease-standard | cubic-bezier(0.645,0.045,0.355,1) | Ein und aus, für den allgemeinen Gebrauch |
--ran-motion-ease-snappy | cubic-bezier(0.33,0,0.15,1) | Schnell, ohne Überschwingen: Schalter |
--ran-motion-ease-spring | cubic-bezier(0.34,1.26,0.5,1) | Leichtes Überschwingen: Schaltflächen, Karten |
--ran-motion-ease-bouncy | cubic-bezier(0.34,1.56,0.64,1) | Verspieltes Überschwingen: Gefällt mir, In den Warenkorb |
--ran-motion-ease-smooth | cubic-bezier(0.4,0,0.2,1) | Ruhig, ohne Überschwingen: Einblendungen, Layout |
Die spring-Familie ist aus abgestimmten SwiftUI-Federn destilliert (Response und Dämpfung auf eine Bézier mit einem einzigen Überschwingen eingedampft).
Kombiniere sie nur mit Bewegungseigenschaften: transform, opacity, die Geometrie des Kastens. Paletteneigenschaften (background-color, color, border-color, box-shadow, fill, stroke) tragen bewusst keinen Standardübergang, denn CSS kann eine Interaktion nicht von einem Themenwechsel unterscheiden: Jede Überblendung, die du einer Farbe gibst, feuert auch beim Wechsel zwischen hell und dunkel. Jede Komponente bietet trotzdem einen --ran-*-transition-Haken, falls du es doch willst.
Fokus
| Token | Wert | Wofür |
|---|---|---|
--ran-focus-ring | 0 0 0 2px var(--ran-background-100), 0 0 0 4px var(--ran-blue-700) | Der Standardring, als box-shadow |
--ran-focus-ring-inverse-color | #fff | Die Ringfarbe für eine Fläche, die in beiden Themes dunkel ist |
Der Ring hat zwei Lagen: einen inneren in der Hintergrundfarbe und einen äußeren in Blau. So bleibt er auf jeder Fläche sichtbar und bleibt blau, statt dem inzwischen monochromen Primär zu folgen.
--ran-focus-ring-inverse-color wird bewusst nicht im Dunkelmodus neu definiert: Es gibt ihn für eine Komponente, deren eigene Fläche unabhängig vom Seitenthema fest dunkel ist (die Steuerleiste von r-player, über beliebigem Video), und diese Fläche ändert sich nicht, wenn die Seite es tut.
Skin-Primitive
Die wenigen strukturellen Werte, die Komponenten teilen und die weder Farbe noch Größe noch Schrift sind. Bewusst knapp gehalten: Diese Ebene war einmal viel größer, und das meiste davon fiel mit den Theme-Paketen weg.
| Token | Wert | Wofür |
|---|---|---|
--ran-skin-border-width | 1px | Die Rahmenstärke, die Komponenten zeichnen |
--ran-skin-border-style | solid | Der Rahmenstil, den Komponenten zeichnen |
--ran-skin-border-image-width | 4px | Der Einzug von border-image-slice, geteilt von button/checkbox/input/modal/message |
--ran-skin-raised-shadow | var(--ran-shadow-elevated) | Der Schatten gehobener Flächen, indirekt, damit ein Skin ihn ändern kann |
--ran-skin-font-family | var(--ran-font-family) | Die Schriftfamilie der Komponenten, auf dieselbe Weise indirekt |
Was der Dunkelmodus neu definiert
data-ran-theme="dark" an <html> (oder an einem beliebigen Teilbaum, siehe Themengestaltung) definiert die Basispalette neu und sonst nichts, mit drei Ausnahmen, die sich nicht über eine Skala auflösen lassen:
- die gesamte Ebene 1: jede Sprosse von Grau, Grau-Alpha, Blau, Rot, Bernstein und Grün sowie beide Hintergründe;
--ran-color-bg-elevated, das im Dunklen auf--ran-gray-100zeigt, damit sich eine Karte von einer schwarzen Seite abhebt, statt darin zu verschwinden;--ran-color-primary-hover/-active, die Literale sind statt Verweise auf eine Skala;- alle drei Schattenstufen, für einen dunklen Grund neu abgestimmt.
Alles andere (jedes weitere semantische Token, jede Größe, jede Dauer) ist genau einmal definiert.
Komponenten-Tokens
Unterhalb der semantischen Ebene stellt jede Komponente eigene Haken bereit, benannt als:
--ran-{component}-{element}[-{state}]-{property}zum Beispiel --ran-btn-hover-background, --ran-select-search-active-border-width. Sie fallen standardmäßig auf semantische Tokens zurück: var(--ran-btn-background, var(--ran-color-primary, #171717)). Ein semantisches Token zu überschreiben erreicht sie also alle, ein Komponenten-Token zu überschreiben verengt die Änderung auf ein einzelnes Element.
Die vollständige erzeugte Liste ist style-tokens-public.md im Repository; die API je Element steht hier. Wie man sie anwendet, zeigt die Themengestaltung.
Tokens im eigenen CSS verwenden
.panel {
background: var(--ran-color-bg-elevated);
color: var(--ran-color-text);
border: var(--ran-skin-border-width) var(--ran-skin-border-style) var(--ran-color-border);
border-radius: var(--ran-radius-md);
padding: var(--ran-space-4);
box-shadow: var(--ran-shadow-elevated);
}Drei Regeln halten das im Dunkeln sicher:
- Kein roher Hex-Wert für etwas, das dem Theme folgen soll.
- Ein Rückfallwert muss ein Token nennen, das mitwechselt:
var(--ran-color-text, var(--ran-gray-1000)), niemalsvar(--ran-color-text, #171717). - Ein Rückfallwert muss ein Token nennen, das es gibt, sonst fällt die Deklaration weg und das Element behält stillschweigend, was es geerbt hat.
Jedes globale Token, das die Bibliothek deklariert, steht auf dieser Seite, und ein Unit-Test schlägt fehl, wenn eines hinzukommt, ohne hier dokumentiert zu sein. Komponentenbezogene Tokens werden getrennt erzeugt, in style-tokens-public.md.