Theming on Lynx
Every styling failure on this page has the same shape. The CSS is valid, the browser preview is correct, the console stays empty, and the device renders something wrong, because Lynx drops what its engine cannot handle instead of rejecting it.
The Theming section documents the API. This page is the layer underneath, where each constraint explains a Vy UI decision that looks arbitrary until you hit it.
Lynx resolves one level of var()
A browser resolves custom properties recursively, so a chain of tokens pointing at tokens works fine. Lynx resolves a single level, and a longer chain evaluates to nothing, which surfaces as white backgrounds and black borders.
So our semantic tokens hold concrete values:
/* packages/kit/src/style.css — concrete, not var(--ui-color-slate-500) */
--ui-text-muted: theme('colors.slate.500');
--ui-bg-muted: theme('colors.slate.50');
--ui-text-muted: var(--ui-color-neutral-500) would read more nicely and previews perfectly in a browser, then collapses on a phone. Dark mode flips these tokens to their own concrete values rather than repointing them at a different chain.
var() does resolve inside linear-gradient(), which we have confirmed on device, so gradient stops can stay as tokens and flip with .dark for free.
Rule for your own themes: a custom property may hold a value, but it may not hold another custom property.
Most Tailwind utilities do not exist
@lynx-js/tailwind-preset allowlists 57 of Tailwind's 179 core plugins and disables the rest, since a class the engine cannot parse is a silent no-op. The Tailwind on Lynx guide covers the mechanism and the v3 pin that comes with it.
The ring family is among the casualties, so a focus ring cannot be ring-2 ring-primary-200. A spread-only box-shadow works instead, with a single-level var() inside it:
shadow-[0_0_0_2px_var(--ui-color-primary-200)]
We track focus in JavaScript, because :focus-visible is not supported either.
data-[state] selectors do not match
Component libraries on the web style state with data-state="open" and matching data-[state=open]: variants, which Lynx does not match, so those variants compile to CSS that never applies.
Vy UI keeps data-state because assistive technology and consumers read it, and drives styling from parallel ui-* classes generated by the preset's uiVariants plugin. Registering the kit's extra states is an easy step to miss in installation:
import { createVyuiPreset, VYUI_UI_STATES } from '@vyui/kit/tailwind'
const lynxPreset = createLynxPreset({
lynxUIPlugins: {
uiVariants: {
prefixes: defaults => ({ ...defaults, ui: [...defaults.ui, ...VYUI_UI_STATES] }),
},
},
})
VYUI_UI_STATES covers on, off, completed, highlighted, inactive, and dragging. Skipping it leaves components in their default state forever.
Icons ignore text color
Lynx rasterizes SVG rather than rendering it live, so an icon does not inherit color and text-primary-500 on a wrapper does nothing. The color has to be baked in at render time, which is what resolveColorHex exists for:
import { resolveColorHex } from '@vyui/kit'
Size works the same way, through the component's size prop rather than w-* and h-*. Lynx's <svg> support also excludes masks, so mask-based icon sets render as solid boxes and a non-masked variant is the whole fix.
Dynamically built classes need a safelist
A class assembled at runtime, such as `bg-${color}-500`, cannot be extracted by Tailwind's scanner, which only sees literal strings. On Lynx that missing utility degrades to nothing rather than to something visible, so the component renders untinted.
Any color slot you build dynamically has to appear literally somewhere Tailwind scans, or go in safelist. Colors without a shade suffix, like text-primary or bg-success, need the same treatment.
What this adds up to
Four rules cover almost all of it. Keep custom properties one level deep, assume a utility does not exist until you have watched it render on a device, style state through ui-* classes while keeping data-state for accessibility, and give icons an explicit color and size.
When the rules run out, fall back to the shape. Something that looks right in the browser and wrong on the device is usually a declaration Lynx dropped, so a device check before calling a theme finished is mandatory.