Theming
Token namespaces, the theme-dumb principle, color scheme, and Material You dynamic color.
touchcn styling lives entirely in the Tailwind token layer (@touchcn/tailwind). Components apply
semantic classes (.tcn-btn-primary, .tcn-material) and semantic tokens (var(--color-*)) —
never theme-specific values — so a single component works on both platforms in light and dark.
Token namespaces
Every token is defined per platform namespace, with light and dark values:
| Namespace | Applies when |
|---|---|
.ios |
iOS liquid-glass theme |
.md |
Material 3 theme |
.dark |
Dark color scheme (combined, e.g. .md.dark) |
The core AppearanceManager toggles the ios / md and dark classes on <html>. Because every token
has an .ios, .ios.dark, .md, and .md.dark value, components reference var(--color-surface)
and get the right value for the active platform and color scheme automatically.
Customizing the look
Three ways to change how touchcn looks, from broadest to most surgical:
-
Re-tint from a brand color — generate a full scheme from a seed at runtime; see Material You and Accent color (iOS) below.
-
Override tokens in your CSS. Token blocks are plain (unlayered)
.ios/.mdselectors, so the same selectors in your own stylesheet — imported after@touchcn/tailwind— win by source order. Always set all four namespace × scheme combinations so the dark scheme stays correct:.md { --color-primary: #6d28d9; } .md.dark { --color-primary: #a78bfa; } .ios { --color-primary: #5856d6; } .ios.dark { --color-primary: #5e5ce6; } -
Style or edit a single component. Utility classes you put on a component always beat touchcn’s chrome (it lives in a low-specificity
@layer), and for anything deeper the component source is in your repo — edit it. Just follow the rules below so platform adaptation keeps working.
Color scheme
The dark scheme is a class (dark) on <html>, combined with the active platform namespace. The
provider manages it from the AppearanceManager; you toggle it through the framework binding:
import { inject } from '@angular/core';
import { TouchcnAppearance } from '@touchcn/angular';
const appearance = inject(TouchcnAppearance);
appearance.setScheme('dark'); // or 'light'; null follows the systemimport { useTouchcnAppearance } from '@touchcn/react';
const { setScheme } = useTouchcnAppearance();
setScheme('dark'); // or 'light'; null follows the system<script setup lang="ts">
import { useTouchcnAppearance } from '@touchcn/vue';
const { setScheme } = useTouchcnAppearance();
setScheme('dark'); // or 'light'; null follows the system
</script>Material You
On Android you can generate a Material 3 dynamic color scheme from a single seed color and apply it at
runtime. generateMaterialTheme(seed) expands the seed into MD3 tonal roles (light and dark);
applyMaterialTheme() injects a single <style id="touchcn-material-theme"> with cascade-correct
.md and .md.dark token blocks.
import { generateMaterialTheme, applyMaterialTheme, clearMaterialTheme } from '@touchcn/core';
// Re-tint the Material theme from a brand seed color.
applyMaterialTheme(generateMaterialTheme('#6750a4'));
// Restore the hand-written baseline.
clearMaterialTheme();
Never set generated tokens as inline styles — they’d break the .md.dark cascade. The generated
declarations only affect the .md namespace; the iOS theme is unaffected.
Accent color (iOS)
iOS follows a single-accent convention: rather than a full tonal system, only the primary tint is
themed — the semantic roles (success, destructive, warning, surfaces) stay stock. generateIosTheme
mirrors the Material You mechanism exactly: it injects a single <style id="touchcn-ios-theme"> with
cascade-correct .ios and .ios.dark token blocks, so it re-tints the primary while keeping dark
switching intact.
import { generateIosTheme, applyIosTheme, clearIosTheme } from '@touchcn/core';
// Re-tint the iOS accent. The dark-scheme pair is derived automatically
// (iOS dark accents are the lightened, slightly more saturated pair).
applyIosTheme(generateIosTheme({ primary: '#af52de' }));
// Supply the dark accent explicitly when you don't want the derived one:
applyIosTheme(generateIosTheme({ primary: '#007aff', primaryDark: '#0a84ff' }));
// Restore the hand-written baseline (stock iOS system blue).
clearIosTheme();
--color-on-primary is chosen automatically (black or white, whichever has more contrast). The
generated declarations only affect the .ios namespace; the Material theme is unaffected.
.tcn-material
Apply the .tcn-material class to opt a surface into the platform’s material treatment — a tonal
elevated surface under .md, a frosted glass surface under .ios — using the same semantic tokens
described above.
Rules when editing copied components
Copied component source is yours, but the platform adaptation is cascade-driven — these two rules are what keep it working:
- Stay theme-dumb. Components carry no platform conditionals in TypeScript. Platform chrome is
scoped
:where(.ios) .tcn-*/:where(.md) .tcn-*in the token CSS; structurally different markup uses the.if-ios/.if-mdvisibility fork (both variants exist in the DOM, the cascade shows one). Keep the semantictcn-*classes when editing — they are the hook the platform styling attaches to. - Zero
backdrop-filterunder any.mdscope. Glass is iOS-only; Material uses opaque tonal surfaces and elevation. (backdrop-filtercreates a containing block that would breakfixedoverlays.)