Guide Porchlight CSS

Theming

Override design tokens, create brand themes, and understand the light-dark() color system.

52 components 51 stable 1 experimental

Command

Search Porchlight

Theming

Porchlight is fully token-driven. Every color, spacing value, font size, radius, and motion duration is a CSS custom property you can override. This guide covers the token architecture, how to create a brand theme, and how the light/dark system works.

Token hierarchy

Tokens are split into two layers:

LayerPrefixPurpose
Primitive (02-tokens.css)--pl-*Raw design scales: font sizes, spacing steps, OKLCH color ramp, radii, motion, shadows
Semantic (02-tokens.css)--pl-color-*Foreground/background pairs: bg, surface, text, border, accent, danger-*, success-*, warning-*
Component-local--c-*Per-component tokens defined inside @scope (e.g., --c-alert-tone)

Semantic tokens use light-dark() so they automatically adapt to the user’s color scheme. Override primitives to retheme the entire system; override semantic tokens to fine-tune specific roles.

Light and dark

Porchlight uses the CSS color-scheme property to drive light-dark() color pairs. Two ways to control the active scheme:

System preference (default)

Omit the data-theme attribute. The browser follows the OS setting:

<html>
  <!-- follows prefers-color-scheme -->
</html>

Explicit theme

Set data-theme on the root element:

<html data-theme="dark">
  <!-- always dark -->
</html>

Valid values: light, dark, or omit for system.

Creating a brand theme

Override the accent color pair to instantly rebrand. Both the accent and its text color need to meet WCAG-AA contrast (4.5:1 minimum):

@layer app {
  :root {
    /* Your brand color in OKLCH */
    --pl-color-accent: oklch(55% 0.22 290deg);
    --pl-color-accent-text: oklch(98% 0.01 290deg);

    /* The accent ramp is derived from these via color-mix().
       Override individual steps if you need full control: */
    /* --pl-color-accent-300: oklch(...); */
    /* --pl-color-accent-700: oklch(...); */
  }

  /* Dark theme brand override */
  :root[data-theme="dark"] {
    --pl-color-accent: oklch(70% 0.18 290deg);
    --pl-color-accent-text: oklch(20% 0.05 290deg);
  }
}

To go deeper, override the semantic surface/text tokens:

@layer app {
  :root {
    --pl-color-bg: oklch(99% 0.005 260deg);
    --pl-color-surface: oklch(97% 0.008 260deg);
    --pl-color-text: oklch(25% 0.02 260deg);
  }
}

Advanced: full brand ramp

The accent color is derived from a 9-step brand ramp:

--pl-brand-1  (lightest)  →  --pl-brand-9  (darkest)
--pl-color-accent = light-dark(var(--pl-brand-7), var(--pl-brand-4))

To retheme with your brand hue, override the ramp steps. Each step varies lightness while keeping the same hue and chroma direction:

@layer app {
  :root {
    /* Target red: hue 25°  */
    --pl-brand-1: oklch(96% 0.02 25deg);
    --pl-brand-2: oklch(88% 0.05 25deg);
    --pl-brand-3: oklch(79% 0.09 25deg);
    --pl-brand-4: oklch(69% 0.17 25deg); /* dark-mode accent  */
    --pl-brand-5: oklch(62% 0.19 25deg);
    --pl-brand-6: oklch(55% 0.2 25deg);
    --pl-brand-7: oklch(47% 0.21 25deg); /* light-mode accent  */
    --pl-brand-8: oklch(38% 0.16 25deg);
    --pl-brand-9: oklch(29% 0.1 25deg);
  }
}

Three tokens to update together

When you change the brand hue, three tokens need attention:

  1. Brand ramp (--pl-brand-1 through --pl-brand-9) — sets the accent.

  2. Focus ring (--pl-focus-color) — a separate token, not derived from the brand ramp. It defaults to hue 82° (amber). Override it to match:

    --pl-focus-color: light-dark(oklch(50% 0.18 25deg), oklch(70% 0.15 25deg));
  3. Danger hue (--pl-color-danger) — defaults to oklch(60% 0.22 25deg). If your brand is red/orange (hue ~25°), the danger state will visually collide with primary actions. Shift danger to a different hue to maintain semantic clarity:

    --pl-color-danger: oklch(57% 0.23 12deg);
    --pl-color-danger-text: light-dark(
      oklch(36% 0.2 12deg),
      oklch(90% 0.08 12deg)
    );

Corporate vs. consumer feel

The large-surface radius tokens control how “rounded” the framework feels:

@layer app {
  :root {
    /* Corporate / enterprise: tighter corners */
    --pl-radius-xl: 0.5rem;
    --pl-radius-2xl: 0.75rem;

    /* Consumer / playful: more rounded */
    /* --pl-radius-xl: 1.5rem; */
    /* --pl-radius-2xl: 2rem; */
  }
}

These drive card corners, dialog radius, table wrap radius, and toast radius.

Density modes

Four density presets control component geometry:

TierControl heightBest for
compact2rem (32px)Admin panels, settings
comfortable2.5rem (40px)Default — most SaaS apps
touch2.75rem (44px)Mobile, tablets, kiosks
dense1.75rem (28px)SIEM, analytics, log viewers
<div data-density="compact">Tighter controls</div>
<div data-density="comfortable">Default</div>
<div data-density="touch">Larger touch targets</div>
<div data-density="dense">Maximum information density</div>

Set on any ancestor. compact, comfortable, and touch change only --pl-control-block-size, --pl-control-padding-inline, and --pl-control-gap — type, spacing, and radius are unchanged.

The dense tier goes further: it also compresses the --pl-space-* scale (~30% reduction) and small text sizes (--pl-text-sm, --pl-text-xs) for maximum information density. See the Data-dense mode reference for details.

Reduced motion

Porchlight respects prefers-reduced-motion: reduce automatically:

  • --pl-motion-scale drops to 0, which zeroes all duration tokens
  • A universal transition-duration: 1ms !important clamp fires on every element, guaranteeing no stray transitions outlast the preference
  • Scroll behavior reverts to auto

You don’t need to do anything. This is one of the few places !important is used (it’s an accessibility guarantee).

Contrast preference

When the browser reports prefers-contrast: more, Porchlight strengthens the structural tokens components already use:

  • --pl-color-border becomes more visible
  • --pl-color-surface-2 separates stronger from the page background
  • --pl-focus-size increases to 3px
  • --pl-focus-glow-opacity makes field glow rings easier to see

This is a token-level adjustment, not a separate component mode. Apps can still override these tokens in their own layer when a brand needs different contrast behavior.

Forced colors (High Contrast)

Under Windows High Contrast mode, all semantic tokens remap to system colors (Canvas, CanvasText, ButtonBorder, Highlight). The UI remains legible without any action from you.

Component-level theming

Each component defines --c-* tokens inside its @scope. Override these to fine-tune a specific component without touching the global palette:

@layer app {
  .c-card {
    --c-card-pad: var(--pl-space-6);
    --c-card-radius: var(--pl-radius-xl);
  }
}

Token reference

For the complete list of tokens with live values, see the token reference pages.