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*.
flexDirectiondefaults tocolumn, exactly like React Native: a container withgap/justifyContent/alignItems/etc. and no explicit direction stacks its children vertically. (A style with no flex props isn't a flex container at all.)alignItemsdefaults tostretch, 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).alignSelfoverrides the container's alignment per child, includingalignSelf: 'stretch'/'flex-end'inside analignItems: 'flex-start'row.- Numbers are pixels (embedded displays are fixed-density; there's no
dp). - Percentages work:
width: '50%'. position: 'absolute'withtop/left/right/bottomfor overlays — all four insets, in styles or as element props (bottom: 5, right: 5pins to that corner).rotation/scaletransform about the element center (RN behavior); settransformOriginX/Yto pivot elsewhere.lineHeightis LVGL line spacing — extra pixels between lines — not RN's total line height.- Text styles (
color,fontSize, …) inherit through containers intoTextand widget labels (a Checkbox'stext), so one rootcolorsets 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:
.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 */
}
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.