Creating a Custom Theme
How to Create a Custom Theme: A Step-by-Step Guide
Introduction
This guide shows you how to apply and dynamically switch between brand themes in a Play+ application. Built on the Metamorphic pillar and powered by a token-driven architecture, Play+ supports pure JSON-based theming — with automatic derivation of hover, focus, dark mode, and accessibility tokens.
All users need to do is modify a single file: theme.map.json
. No SCSS. No boilerplate.
Overview
Play+ ships with a default color palette, fonts, spacing, and radii — known as global tokens. You don’t need to define these unless you're adding custom values.
To theme your app:
- Pick from the provided tokens in
theme.map.json
- (Optional) Add custom raw values directly in the JSON
- Switch themes at runtime with a single line of code
Folder Structure Example
src/
└── styles/
└── themes/
├── default/
│ └── theme.map.json
├── acme/
│ └── theme.map.json
└── polar/
└── theme.map.json
Step 1: Define Your Brand Theme via JSON
Create a theme.map.json
file for your brand. Each semantic token is mapped either to:
- A Play+ pre-defined global token name (e.g.,
global-blue-500
), or - A raw value (e.g.,
#007BFF
,"Inter, sans-serif"
,"8px"
)
// styles/themes/acme/theme.map.json
{
"color-brand-primary": "#007BFF",
"font-family-body": "Inter, sans-serif",
"radius-md": "8px"
}
✅ That's all you need to do to brand your app. 🎯 Play+ will handle hover, contrast text, focus rings, and dark mode derivations automatically.
Step 2: Load the Theme Dynamically
In your app’s entry point (e.g., main.ts
, index.tsx
, or equivalent), load your desired theme by calling the Play+ theming engine:
// main.ts or index.tsx
import { playTheme } from 'playplus/theme';
playTheme.load("acme");
This tells Play+ to apply the mappings defined in styles/themes/acme/theme.map.json
.
You can switch themes based on tenant ID, user preferences, or URL parameters.
- This will apply
styles/themes/acme/theme.map.json
- Switching themes per tenant, user, or route becomes trivial
Step 3: Default Theme Fallback
If no theme is selected, Play+ automatically falls back to the default theme:
styles/themes/default/theme.map.json
What Play+ Does Behind the Scenes
Once color-brand-primary
is set, Play+ will automatically derive:
color-brand-primary-hover
: (darkened or lightened)color-text-on-brand-primary
: (accessible contrast color)focus-outline-color
: based on contextdark mode variants
: using the same token map with transformations
This reduces errors, increases consistency, and eliminates boilerplate.
Best Practices
✅ Do:
- Only edit
theme.map.json
- Use named semantic roles like
color-brand-primary
,radius-md
,font-heading
- Keep themes self-contained
- Use predictable folder structure (
themes/[brand]/theme.map.json
)
❌ Don’t:
- Modify core Play+ theme internals
- Write hardcoded values in your components
- Duplicate logic for hover, focus, etc.
- Try to derive variants yourself — let Play+ handle that
With this JSON-first approach, Play+ makes it possible to theme entire apps through configuration — with runtime switching, zero SCSS, and built-in intelligence for accessibility and state management.
🎨 Just change the map. Play+ takes care of the rest.