Theming

Change the look of every UI component by editing one CSS file. Components take their colors from theme tokens: CSS variables such as --primary whose values you set.

How it works

src/styles/default.css
page {
  --primary: rgba(24, 24, 27, 1);
  --primary-foreground: rgba(250, 250, 250, 1);
}
<view class="bg-primary p-4">
  <text class="text-primary-foreground">Saved</text>
</view>
  • The tokens are set on page, the root element of every Lynx app (see Glossary), so every element can read them.
  • Each token holds a complete rgba() color. tailwind-plugin.ts maps the classes straight to the variable: bg-primary becomes background-color: var(--primary).
  • dolan init copied both files into src/styles/. They're yours to edit.

The tokens are complete colors because of how Lynx parses them. Lynx accepts only comma-separated hsl(), while the usual shadcn/ui setup produces space-separated hsl(var(--x) / 1). Lynx drops that value without a warning, and the element renders transparent. See CSS on Lynx for the other color rules.

Opacity modifiers don't work

bg-primary/50 and border-destructive/50 don't work. The token is one finished color, so Tailwind has no separate channels to add alpha to.

Use one of these instead:

  • A translucent token. The theme has --destructive-subtle (red at 10%), used as bg-destructive-subtle. Add your own the same way, as shown in Add a token.
  • A solid token. bg-muted makes a subtle track or background.
  • opacity-* on the element. This fades the element and its children together.

Named Tailwind colors keep their modifiers. bg-black/50 compiles to rgb(0 0 0 / 0.5), which Lynx accepts.

Tokens

TokenTailwind classesUsed for
background / foregroundbg-background, text-foregroundThe app background and its text
card / card-foregroundbg-card, text-card-foregroundCards and raised surfaces
primary / primary-foregroundbg-primary, text-primary-foregroundMain buttons, checked controls, your brand color
secondary / secondary-foregroundbg-secondary, text-secondary-foregroundSecondary buttons, button groups
muted / muted-foregroundbg-muted, text-muted-foregroundField backgrounds, tab bars, helper text
accent / accent-foregroundbg-accent, text-accent-foregroundPressed toggles, the selected select item
destructive / destructive-foregroundbg-destructive, text-destructive-foregroundDelete buttons, errors
destructive-subtlebg-destructive-subtleThe destructive alert's background
borderborder-borderBorders and separators
inputborder-input, bg-inputThe select's border, the switch's off track
ringborder-ringInput and textarea focus rings
ring-subtlenoneThe soft outer focus ring, set inline by fields
chart-1 … chart-5bg-chart-1 … bg-chart-5Chart series colors, legend swatches

Every color class works with bg-, text- and border-. The plugin also sets border-color: var(--border) on every element, so a plain border class draws in the theme's border color.

Radius

--radius (default 8px) sets three classes:

  • rounded-lg: var(--radius)
  • rounded-md: calc(var(--radius) - 2px)
  • rounded-sm: calc(var(--radius) - 4px)

Safe area

The plugin also adds padding classes that keep content clear of the notch and home indicator: pt-safe, pb-safe, px-safe, p-safe and the others. The numbered forms add a spacing value on top, so pb-safe-4 is calc(1rem + env(safe-area-inset-bottom)). See Safe area.

Customize your brand

Edit the values in default.css. Write each one as a complete rgba() or hex color.

src/styles/default.css
page {
  --primary: rgba(37, 99, 235, 1);
  --primary-foreground: rgba(255, 255, 255, 1);
  --ring: rgba(37, 99, 235, 1);
  --ring-subtle: rgba(37, 99, 235, 0.4);
  --radius: 12px;
  /* …keep the other tokens… */
}
  • Change the same tokens in dark.css if you use dark mode.
  • Charts draw with a TypeScript copy of these colors, because Lynx's SVG renderer can't read CSS variables. Pass your new values to provideChartTheme(), as shown on Chart.
  • dolan update merges theme changes too, so your edits survive updates.

Add a token

Add the variable to both theme files, then map it in the plugin:

src/styles/default.css
page {
  --success: rgba(22, 163, 74, 1);
  --success-subtle: rgba(22, 163, 74, 0.1);
}
src/styles/tailwind-plugin.ts
colors: {
  // …the existing colors…
  success: {
    DEFAULT: 'var(--success)',
    subtle: 'var(--success-subtle)',
  },
},

You can now use bg-success, text-success and bg-success-subtle.

Start a new theme

npx @blotch/dolan theme ocean

This copies default.css to src/styles/ocean.css. Edit it, then import it in place of default.css in src/styles.css. dolan update doesn't merge themes you create this way.

Dark mode

dark.css has a dark value for every token, under the selector page.dark. AngularLynx creates page for you and has no API for its classes. To switch themes at runtime, move the dark values onto a class you can bind.

1. In src/styles/dark.css, change the selector page.dark to .dark.

2. Bind that class on your root view from LynxTheme:

src/app/app.ts
import { Component, inject } from '@angular/core';
import { LYNX_ELEMENTS, LynxTheme } from '@blotch/angular-lynx';

@Component({
  selector: 'app-root',
  imports: [LYNX_ELEMENTS],
  template: `
    <view class="h-full bg-background" [class.dark]="theme.isDarkMode()">
      <text class="text-foreground">This follows the device theme.</text>
    </view>
  `,
})
export class App {
  protected readonly theme = inject(LynxTheme);
}
  • isDarkMode() is a signal. The host app (the native app that runs your bundle, see Glossary) sets it from the device's appearance setting.
  • Charts default to light colors. Add provideChartTheme({ colorScheme: 'system' }) to make them follow too (see Chart).
  • For a manual toggle, bind [class.dark] to your own signal instead. See Dark mode and LynxTheme.

Variable reference

Generated from default.css and dark.css.

VariableLight (default.css)Dark (dark.css)
--backgroundrgba(255, 255, 255, 1)rgba(9, 9, 11, 1)
--foregroundrgba(9, 9, 11, 1)rgba(250, 250, 250, 1)
--cardrgba(255, 255, 255, 1)rgba(9, 9, 11, 1)
--card-foregroundrgba(9, 9, 11, 1)rgba(250, 250, 250, 1)
--primaryrgba(24, 24, 27, 1)rgba(250, 250, 250, 1)
--primary-foregroundrgba(250, 250, 250, 1)rgba(24, 24, 27, 1)
--secondaryrgba(244, 244, 245, 1)rgba(39, 39, 42, 1)
--secondary-foregroundrgba(24, 24, 27, 1)rgba(250, 250, 250, 1)
--mutedrgba(244, 244, 245, 1)rgba(39, 39, 42, 1)
--muted-foregroundrgba(113, 113, 122, 1)rgba(161, 161, 170, 1)
--accentrgba(244, 244, 245, 1)rgba(39, 39, 42, 1)
--accent-foregroundrgba(24, 24, 27, 1)rgba(250, 250, 250, 1)
--destructivergba(239, 68, 68, 1)rgba(127, 29, 29, 1)
--destructive-foregroundrgba(250, 250, 250, 1)rgba(250, 250, 250, 1)
--destructive-subtlergba(239, 68, 68, 0.1)rgba(127, 29, 29, 0.1)
--borderrgba(228, 228, 231, 1)rgba(39, 39, 42, 1)
--inputrgba(228, 228, 231, 1)rgba(39, 39, 42, 1)
--ringrgba(24, 24, 27, 1)rgba(212, 212, 216, 1)
--ring-subtlergba(24, 24, 27, 0.4)rgba(212, 212, 216, 0.4)
--chart-1rgba(231, 110, 80, 1)rgba(38, 98, 217, 1)
--chart-2rgba(42, 157, 144, 1)rgba(46, 184, 138, 1)
--chart-3rgba(39, 71, 84, 1)rgba(232, 140, 48, 1)
--chart-4rgba(232, 196, 104, 1)rgba(175, 87, 219, 1)
--chart-5rgba(244, 164, 98, 1)rgba(226, 54, 112, 1)
--radius8px8px