Domphy

Color Roles & Semantic Colors

Semantic vs. literal colors

Never use literal colors ("#3b82f6", "rgba(0,0,0,0.5)") in Domphy elements. The doctor flags these as raw-theme-value. Instead, use semantic color roles via themeColor():

import { themeColor } from "@domphy/theme"

// ✗ Literal — breaks dark mode, doctor flags it
const bad = { div: "Error", style: { color: "#ef4444" } }

// ✓ Semantic — adapts to theme and dark mode
const good = { div: "Error", style: { color: (l) => themeColor(l, "shift-9", "error") } }

// ✓ Even better — use a patch
const best = { span: "Error", $: [small({ color: "error" })] }

Surface roles

Use shift scale anchors for backgrounds and borders:

RoleToneUse
Page background"inherit"Root background
Card background"shift-1"Slightly elevated surface
Input background"shift-2"Form fields, code blocks
Border"shift-3"Dividers, input borders
Muted border"shift-4"Subtle separators
Placeholder text"shift-6"Disabled / hint text
Secondary text"shift-8"Labels, captions
Body text"shift-10"Primary readable content
Heading text"shift-12"High-contrast headings
Icon"shift-9"Action icons
const Card = {
  div: CardContent,
  style: {
    background: (l) => themeColor(l, "shift-1"),
    border: (l) => `1px solid ${themeColor(l, "shift-3")}`,
    borderRadius: (l) => themeSpacing(2),
    padding: (l) => themeSpacing(4),
  },
}

Semantic color families

Use color families for meaning, not decoration:

FamilyWhen to use
"primary"Main actions, links, active states
"secondary"Secondary actions, alternative paths
"success"Completed, confirmed, positive
"warning"Caution, degraded state
"error" / "danger"Failed, destructive, critical
"info"Informational, neutral notices
"attention" / "highlight"Callouts, promotions
"neutral"Default, disabled, placeholder
const StatusBadge = (status: "success" | "warning" | "error") => ({
  span: status,
  style: {
    background: (l) => themeColor(l, "shift-2", status),
    color: (l) => themeColor(l, "shift-11", status),
    border: (l) => `1px solid ${themeColor(l, "shift-5", status)}`,
    padding: (l) => `${themeSpacing(0.5)} ${themeSpacing(2)}`,
    borderRadius: (l) => themeSpacing(1),
    fontSize: (l) => themeSize(l, "decrease-1"),
  },
})

Interactive state colors

Use "increase-N" tone for hover/pressed states:

import { toState } from "@domphy/core"

function ActionButton({ label, onClick }: { label: string; onClick: () => void }) {
  const hovered = toState(false)

  return {
    button: label,
    onClick,
    onMouseEnter: () => hovered.set(true),
    onMouseLeave: () => hovered.set(false),
    style: {
      background: (l) => themeColor(l,
        hovered.get(l) ? "increase-1" : "shift-2",
        "primary"
      ),
      color: (l) => themeColor(l, "shift-11", "primary"),
      border: (l) => `1px solid ${themeColor(l, "shift-6", "primary")}`,
      padding: (l) => `${themeSpacing(themeDensity(l) * 1)} ${themeSpacing(themeDensity(l) * 3)}`,
      cursor: "pointer",
      transition: "background 100ms ease",
    },
  }
}

Focus ring

Standard accessible focus ring using the primary color:

const FocusableInput = {
  input: null,
  type: "text",
  style: {
    outline: "none",
    boxShadow: (l) => `0 0 0 2px ${themeColor(l, "shift-7", "primary")}`,
  },
  // Only show ring when focused
  // Use the CSS :focus-visible selector via a class or :focus-within on parent
}

Overlay colors with opacity

For overlays, shadows, and scrims:

const Overlay = {
  div: null,
  style: {
    position: "fixed",
    inset: 0,
    // Use neutral at high shift with opacity for scrim
    background: (l) => themeColor(l, "shift-15", "neutral"),
    opacity: 0.5,
    zIndex: 100,
  },
}

themeColorToken — concrete hex values

When a third-party library requires a raw hex/rgb (e.g., Chart.js, D3, canvas):

import { themeColorToken } from "@domphy/theme"

// Returns a concrete string like "#4a7ff4" resolved from the theme
const chartColor = themeColorToken(myElement, "shift-9", "primary")

// Use in Chart.js config
const chartConfig = {
  data: {
    datasets: [{
      backgroundColor: themeColorToken(myElement, "shift-3", "primary"),
      borderColor: themeColorToken(myElement, "shift-9", "primary"),
    }],
  },
}

themeColorToken uses CIELAB/LCH matching — the same algorithm that powers @domphy/doctor's raw-theme-value hint.

Color audit

Run the doctor to find literal colors:

import { diagnose } from "@domphy/doctor"

const issues = diagnose(MyApp)
const colorIssues = issues.filter(i => i.rule === "raw-theme-value")

// Each issue includes a hint with the nearest themeColor() equivalent:
// { rule: "raw-theme-value", message: "Use themeColor(l, 'shift-9', 'error') instead of '#ef4444'" }