Theming

Jede Komponente liest dieselben Custom Properties, ein Theme ist also nur ein Satz Überschreibungen auf :root — kein Build-Schritt, keine Konfigurationsdatei und keine Komponente, der man davon erzählen müsste.

Die Styles hineinbekommen

styles() gibt einen <link> auf eine Datei zurück, die der Browser über alle Seiten der Website hinweg cacht. Es gibt nichts zu konfigurieren und nichts zu kopieren: sitelos Plugin liefert sie im Dev-Server aus und schreibt sie in den Build, auf derselben Basis wie die Komponenten-Runtime.

import { styles } from 'sitelo/ui'

head(
  title('Meine Website'),
  styles(),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">

Der Name trägt einen Hash des Inhalts, du kannst sie also immutable ausliefern und trotzdem eine Änderung veröffentlichen. { hash: false } gibt dir ein schlichtes /su/ui.css, base zeigt den Link auf eine Kopie, die du selbst hostest.

{ inline: true } packt stattdessen die ganze Datei in ein <style> — rund 11 kB gzippt in jeder Seite, dafür keine zusätzliche Anfrage und nichts, was in dist/ fehlen kann. Für eine einzelne Seite ist das der bessere Handel; der Link holt seine Anfrage ab der zweiten gelesenen Seite wieder herein.

head(
  title('Meine Website'),
  styles({ inline: true }),
)

Der Rest der Familie reicht dir die Einzelteile. stylesheet() gibt das rohe CSS als String zurück — für eine Datei irgendwo, wo sitelo nicht hinkommt, oder um sie selbst irgendwohin zu schreiben —, und stylesUrl() nur die URL, für ein eigenes link-Element.

Presets

Ein Preset ändert den ganzen Look auf einmal. styles({ preset: 'neumorphism' }) verlinkt direkt nach dem Kern-Stylesheet ein zweites — ausgeliefert, gehasht und gecacht zu denselben Bedingungen —, und jede Komponente auf der Seite folgt ihm, ohne dass du am Markup etwas änderst.

import { styles } from 'sitelo/ui'

head(
  title('Meine Website'),
  styles({ preset: 'neumorphism' }),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">
// <link rel="stylesheet" href="/su/neumorphism-5d0e7b91.css">

Jedes Preset hat eine eigene Seite, auf der jede Komponente live neu gestaltet wird: Neumorphismus, Neubrutalismus, Superneon.

inline bettet beide Stylesheets ein, stylesheet({ preset }) gibt sie als einen String zurück, und ein Name, der kein Preset ist, wirft einen Fehler, der die vorhandenen auflistet.

Tokens überschreiben

theme() schreibt die Überschreibungen. Schlüssel sind Token-Namen in camelCase, Paletten-Objekte oder wörtliche Custom Properties — und es kommt nach styles(), gewinnt also.

import { styles, theme } from 'sitelo/ui'

head(
  styles(),
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', active: '#3f3fb0', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }),
)

Eingegrenzte Themes

Ein selector grenzt die Überschreibungen auf einen Teilbaum statt auf die ganze Seite ein. Genau das tun die drei Panels unten — dieselben Komponenten, drei verschiedene Paletten, eine Seite.

indigo
pink
rund
fragment(
  theme({ primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff', soft: '#e6e6fa', softFg: '#33338f', border: '#b9b9ee' } }, { selector: '.theme-indigo' }),
  theme({ primary: { base: '#b0357a', hover: '#962e68', fg: '#ffffff', soft: '#fbe4f0', softFg: '#7d1f53', border: '#f0a9ce' } }, { selector: '.theme-pink' }),
  theme({ radiusMd: '999px', radiusLg: '1.5rem' }, { selector: '.theme-round' }),
  grid({ min: '11rem' },
    div({ class: 'theme-indigo' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'indigo'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
    div({ class: 'theme-pink' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'pink'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
    div({ class: 'theme-round' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'rund'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
  ),
)

Dunkelmodus

Dunkel löst sich von selbst über prefers-color-scheme auf. Ein explizites data-theme oder data-su-theme mit light oder dark an irgendeinem Vorfahren übersteuert das — so folgen die Demos auf dieser Website dem Umschalter in der oberen Leiste.

Übergib dark für Überschreibungen, die nur dort gelten sollen. Das deckt Attribut und Media Query in einem Rutsch ab.

theme({
  primary: { base: '#5b5bd6' },
}, {
  dark: { primary: { base: '#8f8ff0' } },
})

Was es zu überschreiben gibt

Fünf Paletten mit je neun Plätzen, eine Abstandsskala, Typografie, Radien, Schatten und die Flächenfarben. Jedes davon ist eine Custom Property — öffne das Stylesheet oder den Inspektor deines Browsers, sie stehen alle auf :root.

primary
neutral
success
warning
danger
xs
sm
md
lg
xl
stack({ gap: 'md' },
  stack({ direction: 'row', gap: 'sm', wrap: true },
    ...['primary', 'neutral', 'success', 'warning', 'danger'].map((color) =>
      stack({ gap: 'xs', align: 'center' },
        div({ style: 'width: 3.5rem; height: 2rem; border-radius: 0.4rem; background: var(--su-' + color + ')' }),
        text({ variant: 'caption', tone: 'muted' }, color),
      ),
    ),
  ),
  stack({ direction: 'row', gap: 'sm', wrap: true, align: 'flex-end' },
    ...['xs', 'sm', 'md', 'lg', 'xl'].map((step) =>
      stack({ gap: 'xs', align: 'center' },
        div({ style: 'width: var(--su-space-' + step + '); height: 2rem; border-radius: 0.2rem; background: var(--su-neutral)' }),
        text({ variant: 'caption', tone: 'muted' }, step),
      ),
    ),
  ),
)

Benennung

Ein camelCase-Schlüssel wird zu einer kebab-case-Property: radiusMd ist --su-radius-md, fontSans ist --su-font-sans. Ein verschachteltes Objekt entfaltet sich genauso — { primary: { softFg: … } } setzt --su-primary-soft-fg —, und ein Schlüssel, der bereits mit -- beginnt, wird exakt so verwendet, wie er dasteht: die Notluke für alles, was die Abbildung nicht abdeckt.

Eine Palette hat neun Plätze: base, hover, active, fg, soft, softHover, softFg, border und ring. Setze nur die, die du änderst.

Kontrast

Die mitgelieferten Paletten erfüllen WCAG AA gegen die Flächen, auf denen sie sitzen, in beiden Themes, und ein Test im Repository lässt den Build scheitern, sobald das nicht mehr stimmt. Ein eigenes Theme deckt er nicht ab — prüfe dein fg gegen dein base, bevor du es ausspielst.

Props

styles():

PropTypStandardBeschreibung
preset'neumorphism' | 'neubrutalism' | 'superneon'—Jede Komponente mit einem Preset neu gestalten, nach dem Kern-Stylesheet verlinkt oder eingebettet.
inlinebooleanfalseDas CSS selbst ausgeben statt eines Links darauf.
hashbooleantrueHash des Inhalts in den Dateinamen aufnehmen. Nur verlinkt.
basestring'/su/'Zeigt die URL woandershin; diese Kopie hostest du selbst. Nur verlinkt.
minifybooleantrueKommentare und Leerraum entfernen. Nur inline.
noncestring—CSP-Nonce für das erzeugte Element.

stylesUrl() nimmt base und hash, stylesheet() nimmt minify und preset.

theme(tokens, options):

PropTypStandardBeschreibung
selectorstring':root'Grenzt die Überschreibungen auf einen Teilbaum ein.
darkobject—Überschreibungen, die nur im Dunkelmodus gelten.
noncestring—CSP-Nonce.