Skip to main content
Version: v0.25.0 (Latest)

Theming & Branding

The IDK includes a cross-platform theming system that manages colors, typography, spacing, motion, and branding from a single token-based configuration. Themes are defined once and consumed by Jetpack Compose, React, and web applications through platform-specific integrations.

The system is built around design tokens, named values like color.primary or spacing.4 that are resolved at runtime based on scope, variant (light/dark), and accessibility preferences. Instead of scattering hex codes and pixel values throughout your components, you reference tokens by name. The theming system resolves those names to concrete values based on who is viewing the app, which tenant they belong to, and whether they prefer dark mode or high contrast.

Core Concepts

Theme Definitions

A ThemeDefinition is a named collection of tokens:

data class ThemeDefinition(
val id: String,
val name: String,
val variant: ThemeVariant, // LIGHT, DARK, HIGH_CONTRAST
val scope: ThemeScope, // SYSTEM, APP, TENANT, PRINCIPAL
val tokens: List<ThemeToken>,
val parentId: String?, // Inherit from parent definition
val appId: String?,
val version: Long,
)

Definitions are layered by scope. A TENANT definition overrides an APP definition, which overrides SYSTEM defaults. Within each scope, tokens are merged; you only need to define the tokens you want to override.

Theme Tokens

Every visual property is expressed as a token with a type:

data class ThemeToken(
val key: String, // e.g., "color.primary"
val value: String, // e.g., "#6750A4" or "{color.primaryContainer}"
val type: ThemeTokenType, // COLOR, DIMENSION, FONT_FAMILY, SPACING, DURATION, etc.
)

Token values can reference other tokens using {token.key} syntax. References are expanded during resolution, so {color.primary} resolves to whatever the current theme's primary color is.

Token Categories

CategoryKey PatternExamples
Colorscolor.*color.primary, color.onPrimary, color.surface, color.error
Typographytypography.*typography.fontFamily, typography.displayLarge.fontSize, typography.bodyMedium.lineHeight
Spacingspacing.*spacing.0 (0px) through spacing.48 (192px), 4px grid with 17 stops
Borderborder.*border.none, border.thin (1px), border.medium (2px), border.thick (4px)
Shapeshape.radius.*shape.radius.none through shape.radius.full (9999px)
Elevationelevation.*elevation.none, elevation.xs, elevation.sm through elevation.xl
Motionmotion.*motion.duration.fast (100ms), motion.duration.normal (250ms), motion.easing.standard
Brandingbranding.*branding.appName, branding.primaryColor, branding.logoUrl, branding.faviconUrl

Scope Hierarchy

The scope hierarchy is how the theming system supports multi-tenant white-labeling without duplicating entire theme definitions. Each scope layer only needs to define the tokens it wants to override; everything else falls through to the parent scope. For example, a tenant can change the primary color and logo without redefining every spacing and typography token.

Tokens resolve in order of specificity:

ScopePurpose
SYSTEMIDK defaults: Material Design 3 baseline theme
APPApplication-level branding: your app's color scheme and typography
TENANTTenant-level overrides: white-labeling for multi-tenant applications
PRINCIPALUser-level preferences: individual user customizations

Lower scopes override higher ones. A tenant can override the app's primary color without redefining the entire theme.

Variants

Each scope can define tokens for multiple variants. You do not need to define all three; the resolver falls back to LIGHT if a variant-specific token is missing. The HIGH_CONTRAST variant is specifically for accessibility, where contrast ratios are boosted beyond the standard WCAG AA thresholds.

  • LIGHT: standard light theme
  • DARK: dark theme
  • HIGH_CONTRAST: accessibility variant with boosted contrast ratios

The active variant is selected at runtime based on platform preferences (e.g., prefers-color-scheme on web, system dark mode on iOS/Android).

Color Configuration

The theming system supports four approaches to color configuration, from simple to fully custom:

Seed Color

Generate a full Material Design 3 palette from a single brand color:

val colorConfig = ThemeColorConfig.SeedColor(
seed = "#1a73e8" // Your brand color
)

The seed is converted to the HCT (Hue, Chroma, Tone) color space to generate tonal palettes for primary, secondary, tertiary, neutral, and error roles.

Multi-Seed

Provide separate seed colors for each role:

val colorConfig = ThemeColorConfig.MultiSeed(
primary = "#1a73e8",
secondary = "#34a853",
tertiary = "#fbbc04",
neutral = "#5f6368",
error = "#ea4335",
)

Explicit Palettes

Hand-craft every palette stop. This is useful when your design system specifies exact values (e.g., exported from Figma):

val colorConfig = ThemeColorConfig.ExplicitPalettes(
palettes = DesignSystemPalette(
brand = PaletteScale(
s50 = "#e8f0fe", s100 = "#d2e3fc", s200 = "#aecbfa",
s300 = "#8ab4f8", s400 = "#669df6", s500 = "#4285f4",
s600 = "#1a73e8", s700 = "#1967d2", s800 = "#185abc",
s900 = "#174ea6"
),
// secondary, neutral, error, success, warning, info, pending...
)
)

Palette scales use role-based names (brand, secondary, neutral) rather than color names (blue, green, gray) to keep the design system independent of specific hues.

Hybrid

Use explicit palettes for brand colors (from your design system) and M3-generated palettes for everything else:

val colorConfig = ThemeColorConfig.Hybrid(
explicitPalettes = DesignSystemPalette(
brand = myBrandPalette,
// only define the roles you have explicit values for
),
fallbackSeed = "#1a73e8" // M3 generation for missing roles
)

Theme Resolution

The ThemeResolver combines definitions from all scopes into a ResolvedTheme:

val resolver: ThemeResolver = session.graph.themeResolver

val resolved: ResolvedTheme = resolver.resolve(
variant = ThemeVariant.LIGHT,
scope = ThemeScope.TENANT,
tenantId = "acme-corp"
)

// Access resolved tokens
val primaryColor: String? = resolved.tokens["color.primary"] // "#1a73e8"
val fontFamily: String? = resolved.tokens["typography.fontFamily"]
val branding: BrandingMetadata = resolved.branding

Resolution applies token references, flattens scope layers, and validates the final token set.

The ThemeResolver and ThemeStore are session-scoped and injected via Metro:

@ContributesBinding(SessionScope::class, binding = binding<ThemeResolver>())
class DefaultThemeResolver(private val store: ThemeStore) : ThemeResolver

Compose Integration

On Android and Kotlin Multiplatform, the Compose integration translates IDK tokens into native Material 3 types (ColorScheme, Typography, Shapes). Your composables use standard M3 APIs like MaterialTheme.colorScheme.primary and the values come from the IDK token system automatically.

The lib-conf-theme-compose module maps resolved tokens to Jetpack Compose's Material 3 theme system.

Basic Setup

@Composable
fun MyApp() {
DefaultTheme {
// Material 3 theme is configured from IDK tokens
// All M3 components use the themed colors, typography, and shapes
Scaffold {
Text("Themed content")
}
}
}

With zero configuration, DefaultTheme uses the M3 baseline (purple seed, #6750A4). To apply your brand:

@Composable
fun MyApp() {
DefaultTheme(
colorConfig = ThemeColorConfig.SeedColor("#1a73e8"),
appName = "My App",
logoUrl = "https://example.com/logo.svg"
) {
MyContent()
}
}

CompositionLocals

DefaultTheme provides several CompositionLocal values that components can read:

LocalTypeContent
LocalThemeTokensMap<String, String>Raw flat token map
LocalResolvedThemeResolvedTheme?Full resolved theme object
LocalThemeVariantThemeVariant?Current variant (light/dark)
LocalBrandingTokensBrandingTokensApp name, logo URLs, primary color
LocalSpacingTokensSpacingTokensSpacing scale values
LocalMotionTokensMotionTokensAnimation durations and easings
LocalShadowTokensShadowTokensElevation shadow values
LocalBorderTokensBorderTokensBorder width and radius values
LocalPaletteTokensPaletteTokensRaw palette primitives (tier 0)
LocalAccessibilityStateAccessibilityStateOS accessibility preferences
LocalWindowWidthSizeClassWindowWidthSizeClassResponsive breakpoint class

Token Mappers

Under the hood, DefaultTheme uses a pipeline of mappers to convert IDK tokens into Compose types:

  • ThemeTokenMapper: tokens to M3 ColorScheme
  • TypographyTokenMapper: tokens to M3 Typography (font size, weight, line height, letter spacing)
  • ElevationTokenMapper: tokens to elevation values
  • SpacingTokenMapper / ShadowTokenMapper / BorderTokenMapper / MotionTokenMapper: tokens to structured data classes

Accessibility

The Compose integration automatically adapts to platform accessibility settings:

  • High contrast mode: HighContrastTokenResolver boosts color contrast ratios when the OS reports high-contrast preference
  • Reduced motion: MotionTokens reflect the user's reduced-motion preference
  • Adaptive sizing: AdaptiveTokenResolver adjusts tokens based on WindowWidthSizeClass for responsive layouts

UI Components

The lib-ui-compose module provides pre-built Compose components that consume theme tokens:

ComponentVariants
ButtonPrimary, Secondary, Ghost, Outline
CardDefault, interactive (with click)
InputText input with focus states
SelectDropdown select with menu
CheckboxChecked/unchecked with label
Radio / RadioGroupSingle and grouped selection
BadgePrimary, Error
ModalDialog overlay with backdrop
TabsTab bar with active indicator
ToastPrimary, Success, Error, Warning, Info
BlobExplorerFile tree browser component

Components are styled through a ComponentTheme composable that maps resolved tokens to component-specific token data classes:

@Composable
fun MyApp() {
DefaultTheme(colorConfig = ThemeColorConfig.SeedColor("#1a73e8")) {
ComponentTheme {
// Components now read their tokens from CompositionLocals
Button(onClick = { }) { Text("Submit") }
Card { Text("Card content") }
}
}
}

Each component reads its styling from a dedicated CompositionLocal (e.g., LocalButtonTokens, LocalCardTokens), so you can override individual component tokens without affecting others.

Web and React Integration

The web integration takes a different approach from Compose. Instead of mapping tokens to framework-specific theme objects, it generates CSS custom properties (e.g., --color-primary) that you consume in your stylesheets. This means the same token definitions drive both Compose and web UIs, but the integration mechanism matches what each platform expects.

React ThemeProvider

The React theme SDK provides a ThemeProvider that generates CSS custom properties from the same token system:

import { ThemeProvider } from '@sphereon/theme-react';

function App() {
return (
<ThemeProvider
colorConfig={{ type: 'seed', seed: '#1a73e8' }}
appName="My App"
logoUrl="/logo.svg"
defaultMode="system"
>
<MyContent />
</ThemeProvider>
);
}

The provider:

  • Generates M3 palettes from the color config (same HCT algorithm as the Kotlin implementation)
  • Injects CSS custom properties onto :root (e.g., --color-primary, --spacing-4)
  • Resolves token references
  • Persists the selected mode (light/dark) in localStorage
  • Respects prefers-color-scheme media query when mode is "system"

CSS Custom Properties

IDK token keys map to CSS variables by replacing dots with dashes:

Token KeyCSS Variable
color.primary--color-primary
typography.bodyMedium.fontSize--typography-body-medium-font-size
spacing.4--spacing-4
motion.duration.normal--motion-duration-normal

Use them in your CSS or styled components:

.my-button {
background-color: var(--color-primary);
padding: var(--spacing-3) var(--spacing-4);
border-radius: var(--shape-radius-md);
transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
}

Kotlin/JS Web Utilities

The lib-conf-theme-web module provides Kotlin/JS utilities for server-rendered or non-React web applications:

  • CssTokenMapper: converts token keys to CSS variable names and handles unit conversion (dp/sp to px)
  • WebSystemDefaults: pre-resolved token maps for light and dark baselines
  • FoucPreventionScript: generates a blocking inline <script> that applies the stored theme mode before first paint, preventing flash of unstyled content

React UI Components

A parallel React component library mirrors the Compose components, styled with CSS modules that read from the injected CSS custom properties:

  • Button, Card, Input, Select, Checkbox, Radio, RadioGroup
  • Modal, Tabs, Badge, Toast
  • BlobExplorer

Branding

The branding subsystem provides structured metadata for application chrome (logo, app name, favicon, and custom fonts):

data class BrandingMetadata(
val appName: String?,
val primaryColor: String?,
val logoUrl: String?,
val logoDarkUrl: String?, // Separate logo for dark mode
val faviconUrl: String?,
val fontResourceId: String?, // Custom font reference
val logoResourceId: String?,
val logoDarkResourceId: String?,
)

Branding tokens are set through the standard token system (branding.appName, branding.logoUrl, etc.) and resolved alongside other tokens. In Compose, access them via LocalBrandingTokens; in React/web, read the corresponding CSS variables or the ThemeContext.

For web applications, WebBrandingMetadata adds web-specific fields:

data class WebBrandingMetadata(
val customCssUrl: String?, // External CSS for further customization
val fontStylesheetUrls: List<String>, // Google Fonts or self-hosted font URLs
val assetBaseUrl: String?, // Base URL for logo/favicon assets
)

Custom CSS is sanitized with a configurable policy (CssPolicyConfig) that blocks dangerous selectors and restricts allowed properties.

Multi-Tenant White-Labeling

The scope hierarchy makes multi-tenant white-labeling straightforward. Define your base theme at APP scope, then override per tenant:

val store: ThemeStore = session.graph.themeStore

// App-level base theme
store.save(ThemeDefinition(
id = "app-light",
name = "App Light",
variant = ThemeVariant.LIGHT,
scope = ThemeScope.APP,
tokens = buildTokens {
color(TokenKeyConstants.COLOR_PRIMARY, "#1a73e8")
string(TokenKeyConstants.BRANDING_APP_NAME, "My Platform")
string(TokenKeyConstants.BRANDING_LOGO_URL, "/logos/platform.svg")
}
))

// Tenant override - only the tokens that differ
store.save(ThemeDefinition(
id = "acme-light",
name = "Acme Corp Light",
variant = ThemeVariant.LIGHT,
scope = ThemeScope.TENANT,
tokens = buildTokens {
color(TokenKeyConstants.COLOR_PRIMARY, "#ff5722")
string(TokenKeyConstants.BRANDING_APP_NAME, "Acme Corp Portal")
string(TokenKeyConstants.BRANDING_LOGO_URL, "/logos/acme.svg")
}
))

When resolving for tenant "acme-corp", the primary color is #ff5722 and the logo is Acme's. All other tokens (typography, spacing, secondary colors) fall through to the app-level definition and system defaults.

Dependencies

For Compose applications:

build.gradle.kts
dependencies {
implementation("com.sphereon.idk:lib-conf-theme-core-public:0.25.0")
implementation("com.sphereon.idk:lib-conf-theme-core-impl:0.25.0")
implementation("com.sphereon.idk:lib-conf-theme-compose:0.25.0")

// Optional: pre-built UI components
implementation("com.sphereon.idk:lib-ui-compose:0.25.0")
}

For web applications, install the React packages:

npm install @sphereon/theme-react @sphereon/ui-react

For Kotlin/JS web applications without React:

build.gradle.kts
dependencies {
implementation("com.sphereon.idk:lib-conf-theme-core-public:0.25.0")
implementation("com.sphereon.idk:lib-conf-theme-web:0.25.0")
}