Skip to content
touchcn
Esc
navigateopen⌘Jpreview
On this page

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:

  1. Re-tint from a brand color — generate a full scheme from a seed at runtime; see Material You and Accent color (iOS) below.

  2. Override tokens in your CSS. Token blocks are plain (unlayered) .ios / .md selectors, 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; }
  3. 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 system
import { 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-md visibility fork (both variants exist in the DOM, the cascade shows one). Keep the semantic tcn-* classes when editing — they are the hook the platform styling attaches to.
  • Zero backdrop-filter under any .md scope. Glass is iOS-only; Material uses opaque tonal surfaces and elevation. (backdrop-filter creates a containing block that would break fixed overlays.)

Last updated on August 16, 2026

Was this page helpful?