Skip to main content

Semantic Colors

Semantic colors let your app respond to Light/Dark mode changes without extra code. You define color names that resolve to different hex values depending on the current appearance.

Setting up semantic.colors.json

Create app/assets/semantic.colors.json with your color definitions:

app/assets/semantic.colors.json
{
"surfaceColor": {
"light": "#F9FAFB",
"dark": "#0f172a"
},
"surfaceHighColor": {
"light": "#FFFFFF",
"dark": "#1e293b"
},
"textColor": {
"light": "#111827",
"dark": "#f1f5f9"
},
"textSecondaryColor": {
"light": "#6B7280",
"dark": "#94a3b8"
},
"textMutedColor": {
"light": "#9CA3AF",
"dark": "#64748b"
},
"borderColor": {
"light": "#E5E7EB",
"dark": "#334155"
},
"accentColor": {
"light": "#3B82F6",
"dark": "#60a5fa"
}
}

Each key is a color name. Each value is an object with light and dark hex values.

Using alpha transparency

To include transparency, use the 8-digit hex format (#RRGGBBAA):

{
"overlayColor": {
"light": "#00000033",
"dark": "#00000066"
}
}

Registering in config.cjs

Map the semantic color names to PurgeTSS class names in config.cjs:

purgetss/config.cjs
module.exports = {
theme: {
extend: {
colors: {
surface: {
DEFAULT: 'surfaceColor',
high: 'surfaceHighColor'
},
'on-surface': 'textColor',
'on-surface-variant': 'textSecondaryColor',
muted: 'textMutedColor',
border: 'borderColor',
accent: 'accentColor'
}
}
}
}

This generates utility classes like bg-surface, bg-surface-high, text-on-surface, text-accent, bg-border, etc.

Nesting rules

You can nest one level deep using an object with DEFAULT:

// ✅ Correct — generates bg-surface and bg-surface-high
surface: {
DEFAULT: 'surfaceColor',
high: 'surfaceHighColor'
}
Common error: nested objects without DEFAULT
// ❌ Wrong — generates [object Object] instead of a color
surface: {
regular: 'surfaceColor',
high: 'surfaceHighColor'
}

If you nest without a DEFAULT key and use the base class (bg-surface), PurgeTSS will serialize the object as [object Object]. Always include DEFAULT for the base variant, or use a flat structure.

Flat structure alternative

If you prefer no nesting at all:

colors: {
surface: 'surfaceColor',
'surface-high': 'surfaceHighColor',
'on-surface': 'textColor'
}

Both approaches work. Choose based on how you want to organize your class names.

Using semantic classes in views

<Window class="bg-surface" title="Settings">
<ScrollView class="vertical content-w-screen content-h-auto">
<Label class="text-on-surface font-bold" text="Title" />
<Label class="text-on-surface-variant text-sm" text="Subtitle" />
<View class="h-px w-screen bg-border" />
</ScrollView>
</Window>

When the appearance changes (via Appearance.set() or system toggle), Titanium resolves each semantic color name to its light or dark value on its own.

A minimal semantic palette for most apps:

PurposeSemantic nameLightDarkClasses generated
BackgroundsurfaceColor#F9FAFB#0f172abg-surface
Cards/elevatedsurfaceHighColor#FFFFFF#1e293bbg-surface-high
Primary texttextColor#111827#f1f5f9text-on-surface
Secondary texttextSecondaryColor#6B7280#94a3b8text-on-surface-variant
Borders/dividersborderColor#E5E7EB#334155bg-border
Accent/interactiveaccentColor#3B82F6#60a5fatext-accent, bg-accent
tip

Start with these 5-6 colors. Add more only when the design requires it. Fewer semantic colors means easier maintenance.