CSS on Lynx

Lynx draws your app with its own CSS engine, not a browser's. Most layout and visual CSS carries over, but some defaults differ and some features are missing. This page covers what trips up web developers, and what to write instead.

For every property and value Lynx accepts, see the Lynx CSS reference.

At a glance

On the webOn Lynx
:root, html, bodypage
display: block by defaultdisplay: linear (a vertical stack) by default. flex and grid work
box-sizing: content-box, collapsing marginsborder-box, and margins never collapse
Text anywhereText only inside <text>
color and fonts inheritNothing inherits, except into a nested <text>
1rem is 16px1rem is 14px
overflow: auto scrollsOnly <scroll-view> and <list> scroll
position: fixed pins to the viewportPositions against the page. Use <overlay> for floating UI
:hover, ::before, @media, !importantNot supported
hsl(240 6% 10%)Dropped silently. Write hsl(240, 6%, 10%) or rgba()
Component styles are scopedEvery component rule applies app-wide

The root is page

src/styles.css
page {
  background-color: #ffffff;
  --brand: rgba(79, 70, 229, 1);
}
  • page is the root element of every Lynx app. Its selector does the job of :root and body.
  • html and body match nothing.
  • Put app-wide CSS variables on page. Every element below it can read them.

Layout

.toolbar {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
  • Elements stack vertically by default. Lynx's default display is linear, which lays children out in a column. flex and grid work as on the web. block, inline and inline-block don't exist.
  • flex-direction: row needs display: flex. On a linear element it does nothing. In Tailwind, write flex flex-row, not flex-row alone.
  • Children stretch across their parent. A child with no width fills the parent's cross axis. Add align-self: flex-start (Tailwind self-start) to make a badge hug its content.
  • Boxes are border-box, and margins never collapse: a 16px bottom margin above a 16px top margin makes 32px.
  • min-width and min-height default to 0, not auto. A flex item can shrink below its content and clip its text. Set a min-width to stop it.
  • Every element is position: relative by default, so z-index works on any element.
  • Siblings paint in source order. A later sibling draws on top, even when an earlier one is position: absolute.
  • position: fixed acts like position: absolute against the page. For floating UI such as menus, use <overlay>.
  • position: sticky works only inside a <scroll-view>.
  • overflow is visible or hidden. A <view> never scrolls. Use <scroll-view> or <list>.

Elements and layout covers which element to use for what.

Text

.summary {
  font-size: 15px;
  line-height: 22px;
  text-overflow: ellipsis;
}
<text class="summary" text-maxline="2">
  Your order left the warehouse this morning and arrives on Friday.
</text>
  • Text renders only inside <text>. Text placed directly in a <view> doesn't show.
  • Put text styles on the <text>. A color, font-size or line-height on a <view> doesn't reach the text inside it (see Inheritance).
  • line-height works only on <text>.
  • white-space is normal or nowrap. There's no pre, so spaces and line breaks always collapse.
  • There's no text-transform, so Tailwind's uppercase does nothing. Change the case in TypeScript.
  • There's no text-align: justify.
  • Clamp lines with the text-maxline attribute, as above, instead of -webkit-line-clamp. Pair it with text-overflow: ellipsis: without it, Android draws the extra lines past the box.

Inheritance

<!-- color on the view doesn't reach the text -->
<view style="color: white">
  <text>Not white</text>
</view>

<!-- set it on the text itself -->
<view>
  <text style="color: white">White</text>
</view>
  • CSS properties don't inherit by default. Each element gets only the rules that match it. Inheritance has a performance cost, so Lynx leaves it off.
  • A nested <text> is the exception. An inner <text> takes its parent <text>'s color and font-family.
  • CSS variables always inherit, as on the web. Set them on a parent and use var() anywhere below it.
  • There's no inherit, initial or unset keyword.

To make text properties inherit as on the web, turn on enableCSSInheritance in a lynx.config.ts at the project root:

lynx.config.ts
import { defineConfig, pluginAngularLynx } from '@blotch/angular-lynx-build';

export default defineConfig({
  plugins: [pluginAngularLynx({ enableCSSInheritance: true })],
});

Then color, direction, font-family, font-size, font-style, font-weight, letter-spacing, line-height, text-align, text-decoration and text-shadow inherit. To pick the list yourself, also pass customCSSInheritanceList. Both are in the plugin options on the @blotch/angular-lynx-build page.

Units

UnitOn Lynx
pxA device-independent pixel, as on the web
rem14px, Lynx's root font size. The default font-size is 14px
rpx1/750 of the screen width, so 750rpx is always full width
%, em, vw, vhAs on the web
  • rem is 14px, not 16px. Rem-based sizes come out 12.5% smaller than in a browser. That includes Tailwind's scale: h-11 is 38.5px. Use px where a size must line up with something exact.
  • Lengths need units. Only 0 can go without one, and margin: 5 is ignored. Times need units too: 0ms, not 0.
  • calc() and env() work. min(), max() and clamp() don't.

Colors

FormatExampleWorks
Hex#18181b, #ff000080Yes
Namedred, transparentYes
rgb() / rgba(), commasrgba(24, 24, 27, 0.5)Yes
rgb(), spacesrgb(24 24 27 / 0.5)Yes
hsl() / hsla(), commashsla(240, 5.9%, 10%, 0.5)Yes
hsl(), spaceshsl(240 5.9% 10% / 0.5)No
currentColorNo
  • Space-separated hsl() fails silently. The element draws transparent, with no warning. Lynx's parser reads hsl() only in its comma form.
  • currentColor isn't supported. Use the color itself, or a CSS variable.

Theme colors in CSS variables

Store each color whole in a variable, then reference the variable directly:

src/styles.css
page {
  --primary: rgba(24, 24, 27, 1);
  --primary-foreground: rgba(250, 250, 250, 1);
  --primary-subtle: rgba(24, 24, 27, 0.1);
}

.button {
  background-color: var(--primary);
}

.button-label {
  color: var(--primary-foreground);
}
  • Don't store raw channels (--primary: 240 5.9% 10%) to wrap later in hsl(var(--primary)). That expands to the space-separated hsl() Lynx drops, which is why shadcn-style themes render blank.
  • For a translucent shade, add a variable of its own, like --primary-subtle.
  • Set variables in a stylesheet or a static style attribute. A [style.--name] binding doesn't reach the element yet.

The UI components theme works this way. See Theming.

Selectors and rules

These work: type (text), class, ID, *, compound (.a.b), descendant, child (>), sibling (+, ~), :not() and :active (while a finger is down). The at-rules are @font-face, @keyframes and @import.

MissingUse instead
:hover, :focus:active, or a class bound to a signal
::before, ::afterA real element
:first-child, :nth-child() and other structural classesA class bound to @for's $first, $last or $index
Attribute selectors such as [disabled]A class
:is(), :where(), :has()Separate selectors
@media, including prefers-color-schemeSignals and class bindings. For themes, see Dark mode
@supports, @layer, @container, @propertyNothing
!importantA more specific selector
pointer-eventsLeave it out: the build fails on it. The last sibling gets the taps
float, columnsFlexbox or grid
outlineborder

Descendant selectors don't re-match when a class changes. By default, .selected .label applies only when classes are first set. Toggle a class on the element itself, or switch CSS variables, instead. The enableCSSInvalidation plugin option makes them re-match, at some cost in speed.

Component styles

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

@Component({
  selector: 'app-card',
  imports: [LYNX_ELEMENTS],
  host: { class: 'app-card' },
  template: `<text class="app-card-title">{{ title() }}</text>`,
  styles: `
    .app-card {
      padding: 16px;
      border-radius: 12px;
    }
    .app-card-title {
      font-size: 18px;
    }
  `,
})
export class Card {
  readonly title = input.required<string>();
}

styles and styleUrl work, but component styles aren't isolated on Lynx:

  • ViewEncapsulation.Emulated (the default) keeps your selectors as written. Lynx elements carry no per-component attributes, so a .title rule in one component also matches .title in another. Prefix class names with the component name, as above, or use CSS Modules for names that can't clash.
  • :host and ::ng-deep don't work. To style the host element, give it a class through host: { class: '…' }.
  • ViewEncapsulation.None styles the same way as Emulated.
  • ViewEncapsulation.ShadowDom falls back to None, with a warning at build time and run time. Lynx has no shadow DOM.