Skip to main content

Styling

Kog styling is React Native's StyleSheet model mapped onto LVGL's style system — and the two fit unusually well, because both want you to define styles once and share them.

import { StyleSheet } from '@kog/ui';

const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
gap: 8,
shadowColor: '#000000',
shadowOpacity: 0.1,
},
title: { fontSize: 18, fontWeight: 'bold', color: '#111111' },
});

StyleSheet.create is resolved at build time: colors become integers, enums become numbers, and each unique style becomes a shared native style object — used by every widget that references it, which is exactly how LVGL wants styles used for memory efficiency. The idiom you know is the idiom that's optimal.

Layout is flexbox

LVGL has a real flexbox engine, so RN's layout props map directly:

flexDirection, justifyContent, alignItems, alignSelf, flexWrap, flex, gap/rowGap/columnGap, padding*, margin*, width/height/min*/max*.

  • flexDirection defaults to column, exactly like React Native: a container with gap/justifyContent/alignItems/etc. and no explicit direction stacks its children vertically. (A style with no flex props isn't a flex container at all.)
  • alignItems defaults to stretch, exactly like React Native: children without an explicit cross-axis size fill the container (a { height: 20 } bar spans the full width; justifyContent: 'space-between' headers work without sizing the header).
  • alignSelf overrides the container's alignment per child, including alignSelf: 'stretch' / 'flex-end' inside an alignItems: 'flex-start' row.
  • Numbers are pixels (embedded displays are fixed-density; there's no dp).
  • Percentages work: width: '50%'.
  • position: 'absolute' with top/left/right/bottom for overlays — all four insets, in styles or as element props (bottom: 5, right: 5 pins to that corner).
  • rotation/scale transform about the element center (RN behavior); set transformOriginX/Y to pivot elsewhere.
  • lineHeight is LVGL line spacing — extra pixels between lines — not RN's total line height.
  • Text styles (color, fontSize, …) inherit through containers into Text and widget labels (a Checkbox's text), so one root color sets your app's base ink.

Dynamic styles

Static styles cost nothing at runtime. When style membership depends on state, the element's style list re-applies when the state flips — the departed style's properties un-apply, so nothing goes stale:

<Text style={ok ? styles.pass : styles.fail}>status</Text> // conditional style
<View style={[styles.dot, isOn && styles.dotLit]} /> // conditional merge
<Bar width={`${progress}%`} /> // reactive dynamic prop
<Pressable style={({ pressed }) => pressed && styles.down} /> // interaction states

Style arrays merge left-to-right (later wins); falsy entries (cond && style) are skipped, exactly like React Native. A fully static style list is applied once at mount — zero runtime cost. One rule to know: inline style objects are resolved at build time, so their values must be literals — a state-driven value belongs on a dynamic prop (width={w}), which updates in place instead of minting styles. Interaction states (pressed, focused, disabled, checked) map to LVGL's native state selectors.

For smooth transitions, bind a useAnimatedValue to a prop and call .animate() — native interpolates each frame in C. One asymmetry to know: an animated value is in the prop's native units, so animated opacity runs 0–255, whereas static/reactive opacity is authored 0–1.

CSS modules

Prefer stylesheets? Write real CSS in a *.module.css file and pass classes with className — it's pure compile-time sugar over StyleSheet.create, so the device cost is identical and no CSS ships to the board:

src/cards.module.css
.card {
padding: 4px 10px;
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 12px;
background-color: tomato;
}
.button:active {
background-color: #0d419d; /* LVGL PRESSED state — same machinery as function styles */
}
src/App.tsx
import styles from './cards.module.css';

<View className={styles.card} />
<Pressable className={[styles.card, styles.button]} /> // composition; later wins

Every StyleSheet property has a CSS spelling — standard CSS names where a real equivalent exists (padding-inline, rotate, scale, box-shadow, object-fit, line-clamp, visibility, overflow), kebab-case for kog-only props (tint-color, background-opacity, shadow-width, clip-corner). Colors accept #hex, rgb(), hsl(), and named colors; lengths are px or %. Class selectors, grouped selectors, and the state pseudo-classes :active/:focus/:disabled/:checked are supported — everything outside the subset (descendant selectors, @media, var(), calc(), :hover) is a located compile error with a hint, never a silent drop.

className is build-time static (classes resolve against the parsed CSS) — unlike style, whose conditional membership is reactive. For a state-driven swap put the conditional part on style (className={styles.dot} style={lit && sheet.dotLit}), use a conditional element, and for the pressed look use :active. style wins over className where both set a property. Try the CSS Modules template in Studio, or examples/css-cards in the repo.

Theming and dark mode

const scheme = useColorScheme(); // 'light' | 'dark', reactive
const bg = DynamicColor({ light: '#fff', dark: '#111' });

Wrap your app in <ThemeProvider> to define palette tokens once; theme changes propagate reactively — the shared native styles are swapped, not re-created per widget.

Fonts and glyph coverage

Text renders with LVGL's built-in Montserrat font, which covers printable ASCII only. Non-ASCII characters — emoji, em-dashes (), typographic quotes, arrows (), bullets () — show as blank boxes on the device and in the simulator. Keep rendered strings ASCII (--, ->, -, straight quotes). Bundling custom fonts for wider glyph coverage is planned.