Skip to main content

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:

  1. Pick from the provided tokens in theme.map.json
  2. (Optional) Add custom raw values directly in the JSON
  3. 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 context
  • dark 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.