---
title: Theming
description: Token namespaces, the theme-dumb principle, color scheme, and Material You dynamic color.
sidebar:
  label: Theming
  icon: palette
---

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](#material-you) and [Accent color (iOS)](#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:

   ```css styles.css
   .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](#rules-when-editing-copied-components) 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:

<CodeGroup>

```ts Angular
import { inject } from '@angular/core';
import { TouchcnAppearance } from '@touchcn/angular';

const appearance = inject(TouchcnAppearance);
appearance.setScheme('dark'); // or 'light'; null follows the system
```

```tsx React
import { useTouchcnAppearance } from '@touchcn/react';

const { setScheme } = useTouchcnAppearance();
setScheme('dark'); // or 'light'; null follows the system
```

```vue Vue
<script setup lang="ts">
import { useTouchcnAppearance } from '@touchcn/vue';

const { setScheme } = useTouchcnAppearance();
setScheme('dark'); // or 'light'; null follows the system
</script>
```

</CodeGroup>

## 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.

```ts
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.

```ts
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.)
