Docs
Tailwind
NSH’s styles follow Tailwind CSS 4. In Rust, Tailwind’s utilities are typed style methods with the same names and values (.p_4() is p-4, .rounded_2xl() is rounded-2xl, ZINC.shade(900) is zinc-900); in TSX, every element accepts the classes themselves in class, resolved natively against Tailwind 4’s default theme. Each example below shows the typed methods first and the classes second:
import { mount, Scroll, signal, Text, View } from "nsh";
function App() {
const count = signal(0);
const busy = signal(false);
const tab = signal("Day");
const tabs = ["Day", "Week", "Month"];
const buttons = () => (
<View class="flex-row flex-wrap gap-2">
<View
onPress={() => count.update((n) => n + 1)}
class="rounded-lg bg-lime-300 px-4 py-2 transition-colors duration-150 motion-reduce:transition-none hover:bg-lime-200 active:bg-lime-400 focus-visible:outline-2 focus-visible:outline-sky-500"
>
<Text class="font-medium text-lime-950">Pressed {count()}×</Text>
</View>
<View
onPress={() => busy.set(!busy())}
class="rounded-lg border border-black/10 bg-white px-4 py-2 shadow-sm transition motion-reduce:transition-none hover:bg-zinc-50 active:scale-95 active:bg-zinc-100 dark:border-[#2c2e33] dark:bg-[#24262a] dark:hover:bg-[#2a2c31] dark:active:bg-[#2d2f34] focus-visible:outline-2 focus-visible:outline-sky-500"
>
<Text class="font-medium text-[#15161a] dark:text-[#eceef1]">{busy() ? "Enable" : "Disable"}</Text>
</View>
<View disabled={busy()} onPress={() => count.set(0)} class="rounded-lg bg-emerald-400 px-4 py-2 enabled:hover:bg-emerald-300 active:bg-emerald-500 disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-sky-500">
<Text class="font-medium text-emerald-950">Reset</Text>
</View>
</View>
);
const tabsRow = () => (
<View class="flex-row rounded-xl border border-black/5 bg-[#ecece7] p-1 dark:border-[#1f2124] dark:bg-[#0c0d0f]">
<Each of={tabs}>
{(t) => (
<View
onPress={() => tab.set(t())}
class="flex-1 items-center rounded-lg py-2 transition-colors motion-reduce:transition-none active:opacity-70 focus-visible:outline-2 focus-visible:outline-sky-500"
classList={{ "bg-white shadow-sm dark:bg-[#2a2c31]": tab() === t(), "hover:bg-black/5 dark:hover:bg-white/5": tab() !== t() }}
>
<Text class="text-sm font-medium" classList={{ "text-[#15161a] dark:text-[#eceef1]": tab() === t(), "text-[#565a61] dark:text-[#8d9198]": tab() !== t() }}>
{t()}
</Text>
</View>
)}
</Each>
</View>
);
const gradient = () => (
<View class="overflow-hidden rounded-2xl bg-linear-to-br from-lime-300 via-emerald-300 to-sky-400 p-5 shadow-xl shadow-lime-300/20">
<Text class="text-xl font-bold text-zinc-950">{tab()} view</Text>
<Text class="mt-1 text-sm text-zinc-950/70">bg-linear-to-br from-lime-300 via-emerald-300 to-sky-400</Text>
</View>
);
const grid = () => (
<View class="grid grid-cols-3 gap-2">
<View class="col-span-2 h-14 items-center justify-center rounded-lg bg-sky-950">
<Text class="text-xs text-sky-200">col-span-2</Text>
</View>
<View class="h-14 rounded-lg bg-sky-900" />
<View class="h-14 rounded-lg bg-sky-800" />
<View class="row-span-2 rounded-lg bg-sky-400" />
<View class="h-14 rounded-lg bg-sky-700" />
<View class="h-14 rounded-lg bg-sky-600" />
<View class="h-14 rounded-lg bg-sky-500" />
</View>
);
const list = () => (
<View class="overflow-hidden rounded-xl border border-black/10 bg-white shadow-sm dark:border-[#2c2e33] dark:bg-[#18191c]">
<Each of={["Inbox", "Drafts", "Sent", "Archive"]}>
{(item) => (
<View class="flex-row items-center justify-between border-t border-black/5 px-4 py-3 first:border-t-0 even:bg-[#fafaf7] active:bg-lime-50 dark:border-[#1f2124] dark:even:bg-[#141518] dark:active:bg-[#24262a]">
<Text class="text-[#15161a] dark:text-[#eceef1]">{item()}</Text>
<Text class="text-[#8c9097] dark:text-[#5a5e65]">›</Text>
</View>
)}
</Each>
</View>
);
const motion = () => (
<View class="flex-row items-center justify-around py-2">
<View class="size-8 animate-spin motion-reduce:animate-none rounded-md bg-lime-300" />
<View class="size-8 animate-bounce motion-reduce:animate-none rounded-full bg-sky-400" />
<View class="h-8 w-14 animate-pulse motion-reduce:animate-none rounded-md bg-[#e1e1db] dark:bg-[#2c2e33]" />
</View>
);
return (
<Scroll class="flex-1 bg-[#f3f3ef] dark:bg-[#101113]">
<View class="gap-5 p-5 pt-6">
<Text class="text-2xl font-bold tracking-tight text-[#15161a] dark:text-[#eceef1]">Tailwind classes</Text>
{/* Two columns from `md:` on (768 points): breakpoints follow the window. */}
<View class="gap-5 md:flex-row md:items-start">
<View class="gap-5 md:min-w-0 md:flex-1">
{buttons()}
{tabsRow()}
{gradient()}
</View>
<View class="gap-5 md:min-w-0 md:flex-1">
{grid()}
{list()}
{motion()}
</View>
</View>
</View>
</Scroll>
);
}
mount(App);use nsh_core::element::palette::ZINC;
view()
.flex_row()
.items_center()
.gap_3()
.rounded_2xl()
.bg(AdaptiveColor::new(white(), ZINC.shade(800)))
.p_4()
.shadow_lg()
.active(|s| s.scale(0.98))
.child(
text("Tailwind on native views")
.text_base()
.font_weight(FontWeight::SEMIBOLD)
.text_color(AdaptiveColor::new(ZINC.shade(900), white())),
)<View class="flex-row items-center gap-3 rounded-2xl bg-white p-4 shadow-lg active:scale-[0.98] dark:bg-zinc-800">
<Text class="text-base font-semibold text-zinc-900 dark:text-white">Tailwind on native views</Text>
</View>Typed methods lower into the same style rules the classes compile to, with no string to parse. For classes, no CSS is generated and nothing runs in JavaScript: the string is passed to the core, which parses each distinct class string once (and caches it), turns it into style rules, and applies them. Variants that depend on state (pressed, dark mode, window width) are re-applied by the core when that state changes, and pressed and focused states are tracked natively, so active: feedback (.active(..) in Rust) has no round trip to your code.
Classes and the style prop produce the same native styles and can be combined; this page covers what is specific to classes and their typed equivalents. The Tailwind reference lists every supported utility.
#class and classList
view()
.rounded_lg()
.px_4()
.py_2()
.transition_colors()
.bg(move || if selected.get() { INDIGO.shade(600) } else { ZINC.shade(200) })
.style_when(busy, |s| s.opacity(0.5))<View
class="rounded-lg px-4 py-2 transition-colors"
classList={{ "bg-indigo-600": selected(), "bg-zinc-200": !selected(), "opacity-50": busy() }}
/>classis a string of classes. It can be dynamic:class={`h-6 rounded bg-${color()}-500`}works, because classes are resolved at run time.classListmaps classes to conditions: a class applies while its value is truthy. It is applied afterclass.- In Rust, a method given a closure or a signal follows it (
.bg(move || ..)),.style_when(condition, |s| ..)applies styles while a condition holds, and.when(condition, |v| ..)decides once, when the element is made.
How conflicts resolve:
- Later classes win over earlier ones in the same string:
"p-2 p-4"isp-4. There is no need fortailwind-merge: a component can append theclassits caller passes. Later methods win in Rust too. - Variant classes apply after plain ones, ordered by Tailwind’s variant order (
hover:beforeactive:,max-*:widest first, breakpoints ascending,dark:last), whatever their position in the string. As in Tailwind, a class’s highest variant decides, then its next highest, and a class with one more variant comes later:dark:hover:bg-a dark:bg-bisbg-awhile hovered in dark mode, andmax-md:p-2 max-lg:p-4isp-2belowmd. As in CSS,odd:bg-a last:bg-bgives an odd last rowbg-a, because Tailwind sortsoddafterlast. stylewins over classes. A style key is the CSS property of its name, the one the classes set, so it replaces only that property:style={{ paddingLeft: 8 }}replacespl-4and keepspr-4.!makes a class important:!p-0.
Rust components (nsh/base’s and those of an app’s own crate) accept class and classList from TSX too; your classes are merged after the component’s own styles and win at equal rank.
#Variants
| Variant | Applies when | Rust |
|---|---|---|
active: | The view is pressed | .active(|s| ..) |
hover: | A pointer is over the view (iPad and Mac pointers) | .hover(|s| ..) |
focus:, focus-visible: | An input has focus | .focus(|s| ..) |
focus-within: | The view or a descendant has focus (the content of an Overlay is a descendant of the view it is in) | .focus_within(..) |
disabled:, enabled: | The element’s disabled prop is true / false | .disabled_style(|s| ..) |
selected: | The element is selected (a Button’s selected, accessibilityState.selected) | .selected_style(..) |
checked: | The element is checked (accessibilityState.checked is true; "mixed" is not checked) | .checked_style(..) |
indeterminate: | The element is indeterminate (accessibilityState.checked is "mixed") | .indeterminate_style(..) |
aria-expanded: | The element is expanded (accessibilityState.expanded is true): a Popover’s or Menu’s trigger while its popup is open | .expanded_style(..) |
aria-pressed: | The element is a toggle button that is on (accessibilityState.pressed is true; "mixed" is not pressed). Not active:, a press in progress | .aria_pressed(..), .pressed_style(..) |
aria-busy:, aria-checked:, aria-selected: | accessibilityState.busy, checked (true, not "mixed") or selected is true, as Tailwind’s aria-* variants: after the pseudo-classes, in the order of their names | |
data-<name>: | The element has the data-* attribute name, as Tailwind’s boolean data-* variants (&[data-range-start]): what a component’s part shows beyond its states, a calendar’s day at the start of a range (data-range-start:). After the aria-* variants, in the order of their names | Interactive::project_data(name, ..) sets one on a component’s part |
group-active:, group-hover:, group-focus:, group-focus-within:, group-disabled:, group-selected:, group-checked:, group-indeterminate:, group-aria-expanded:, group-aria-pressed:, group-aria-busy:, group-aria-checked:, group-aria-selected:, group-data-<name>: | The nearest ancestor with the group class is in that state, or has that data-* attribute | .group() on the ancestor; .group_active(..), .group_hover(..), .group_focus_within(..) inside |
group-active/name: … | The same, for an ancestor marked group/name | .group_named("name"); .group_active_named("name", ..), … |
first:, last:, odd:, even:, only: | The element’s own position among its siblings. To color a row’s label by the row’s position, put last:text-* on the row: the label inherits it | Plain Rust from the row’s index (at.is_first(), at.is_last(), at.index() % 2) |
placeholder: | Styles an input’s placeholder (placeholder:text-zinc-400) | .placeholder_color(..) |
selection: | Colors the selection highlight of an input or a selectable text, and of those inside the element (selection:bg-violet-200) | .selection_color(..) |
dark: | The system appearance is dark | An AdaptiveColor, or .dark(|s| ..) |
sm:, md:, lg:, xl:, 2xl: | The window is at least 640, 768, 1024, 1280, 1536 points wide | .style_when(|| window().width >= px(768.0), |s| ..) |
max-sm:, max-md:, … | The window is narrower than the breakpoint | The same, with < |
min-[600px]:, max-[600px]: | Arbitrary breakpoints | The same |
portrait:, landscape: | The window’s orientation | window().width > window().height |
ios:, android:, web: | The platform | .when(os() == Os::Ios, |v| ..) |
rtl:, ltr: | The layout direction | .direction(LayoutDirection::..) on a view |
motion-safe:, motion-reduce: | The user’s reduce motion setting | reduce_motion() |
Variants combine: dark:active:bg-zinc-700, md:landscape:flex-row. In Rust, an AdaptiveColor in an .active(..) style is dark:active:.
view()
.group()
.flex_row()
.items_center()
.gap_3()
.rounded_xl()
.bg(white())
.p_3()
.active(|s| s.bg(ZINC.shade(50)))
.child(text("Settings").flex_1().text_color(ZINC.shade(900)))
.child(
text("›")
.text_color(ZINC.shade(300))
.transition_transform()
.group_active(|s| s.translate_x(px(4.0))),
)<View class="group flex-row items-center gap-3 rounded-xl bg-white p-3 active:bg-zinc-50">
<Text class="flex-1 text-zinc-900">Settings</Text>
<Text class="text-zinc-300 transition-transform group-active:translate-x-1">›</Text>
</View>disabled on any element (.disabled(..) in Rust) applies disabled: classes (.disabled_style(..)) and removes its press handlers from what is mounted, so it shows no press feedback either:
view()
.disabled(saving)
.on_press(save)
.rounded_lg()
.bg(EMERALD.shade(500))
.px_4()
.py_2()
.active(|s| s.bg(EMERALD.shade(600)))
.disabled_style(|s| s.opacity(0.4))
.child(text("Save").font_weight(FontWeight::MEDIUM).text_color(white()))<View disabled={saving()} onPress={save} class="rounded-lg bg-emerald-500 px-4 py-2 active:bg-emerald-600 disabled:opacity-40">
<Text class="font-medium text-white">Save</Text>
</View>Breakpoints compare against the window (the surface NSH renders into), not the device: in an iPad split view or a resizable window, they follow the window’s width, as window() does.
#What differs from Tailwind on the web
- Units are points.
p-4(.p_4()) is 16 points (1 spacing step = 4pt, 1rem = 16pt). Arbitrary values inpxare points too:p-[13px](.p(px(13.0))) is 13pt. flexis a row, as in CSS:class="flex"lays out its children in a row unless a direction class says otherwise (anywhere in the string:flex-col flexis a column). A style’sdisplay: "flex"and Rust’s.flex()are rows the same way, and a view that sets no display keeps NSH’s default column.blockandflow-rootuse block layout, and so doinlineandinline-block, as a browser lays them out in a flex or grid parent. In Rust,.flex_row()and.flex_col()set the direction.- Text classes on views are inherited.
text-*,font-*,leading-*,tracking-*and the other text utilities on a view (.text_color(..),.font_weight(..), … in Rust) pass to the text and input elements inside it, as CSS inheritance does:<View class="text-white">makes its labels white. - Every view is a box of its own.
overflow-x-hiddenandoverflow-y-hiddenclip both axes, asoverflow-hiddendoes (scrolling is the scroll element’s);invisiblehides the view and everything in it;mix-blend-*blends a view with everything drawn behind it;blockboxes keep their own margins and their children’s apart; anabsoluteview is placed in its nearest positioned ancestor, as in CSS;fixedlays a view out in its root view (other elements are placed asabsolute). - Text keeps its whitespace. Spaces and line breaks in a string are drawn as written, and
whitespace-*sets the wrapping; the JSX compiler collapses the whitespace of literal JSX text, as JSX does. - Transitions animate property groups:
transitionanimates colors, opacity, shadows and transforms,transition-allalso corner radii and border widths; filters, backdrop filters, gradients and layout take their new values at once. focus-visible:isfocus:: it applies whenever the element has focus.- State backgrounds show over gradients. In CSS a gradient covers
background-color, soactive:bg-*on abg-linear-*surface would never be seen; in NSH a background color fromactive:,hover:,focus:orgroup-*:replaces the gradient while that state holds (unless the state sets a gradient of its own, such asactive:from-*). So doactiveStyleand the other state styles, and animated background colors. - Text and touch settings are style keys:
line-clamp-*setsnumberOfLines,text-ellipsisandtext-clipsettextOverflow,truncateboth,select-*setsuserSelectandpointer-events-*setspointerEvents, so they work in state variants and lose to the inlinestyleas every class does;caret-*,placeholder:text-*andselection:bg-*set thecaretColor,placeholderColorandselectionColorstyles the same way. In Rust these areStyledmethods (.number_of_lines(..),.truncate(),.text_overflow(..),.select_text(),.pointer_events(..), …).whitespace-nowrapkeeps a text on one line as a class.object-*set theobjectFitandobjectPositionstyles of images and videos, which a style’s keys replace as any other. - The supported set: utilities and variants of Tailwind 4 that have a native meaning.
peer-*variants, pseudo-elements (before:,after:) and arbitrary properties ([mask-type:luminance]) are reported as unknown classes, and so is every utility NSH has no native meaning for, so a class never means something else than in a browser. Rendering hints (antialiased,will-change-transform,appearance-none,box-border, …) are accepted and change nothing.
Everything else works as documented by Tailwind 4: the color palette, opacity modifiers (bg-black/40), arbitrary values (bg-[#34c759], w-[72px], grid-cols-[1fr_2fr]; on p, px, py, pt, pr, pb, pl and the m utilities also the environment variables, pt-[env(safe-area-inset-top)], pb-[calc(env(keyboard-inset-height)+49px)], see Safe area and Keyboard), fractions (w-1/2, translate-x-1/2), shadows and rings, gradients (bg-linear-to-r, bg-radial, bg-conic with from- / via- / to-), filters, backdrop-blur-*, mix-blend-*, transforms, transition-* and duration-*, animate-spin / ping / pulse / bounce, and grid, each with its typed method in Rust. The platform limits of filters and blending apply to classes too.
#Theme
Classes resolve against Tailwind 4’s default theme. Extend it in nsh.json under "tailwind"; your entries are added to the defaults (or replace the default of the same name):
{
"tailwind": {
"colors": {
"brand": { "50": "#f5f3ff", "500": "#7c3aed", "600": "#6d28d9", "DEFAULT": "#7c3aed" },
"ink": "#1f2328"
},
"radius": { "card": 14 },
"fontFamily": { "display": "Pacifico" },
"breakpoints": { "tablet": 600 }
}
}
With this theme, bg-brand-500, text-brand (the DEFAULT shade), border-ink, rounded-card, font-display and tablet:flex-row are valid classes.
| Key | Values | Makes |
|---|---|---|
colors | A CSS color, or an object of shades ("DEFAULT" for the bare name) | bg-*, text-*, border-*, from-*, ring-*, … |
spacing | Points per spacing step (default 4) | p-*, m-*, gap-*, w-*, … |
rem | Points per rem in arbitrary values (default 16) | |
breakpoints | Points | name: variants |
container | Points | max-w-* sizes |
fontSize | Points, or [size, lineHeight] with the line height a multiple of the size | text-* |
fontWeight | 1–1000 | font-* |
fontFamily | A family name (a system family or an imported font’s name) | font-* |
tracking | Ems | tracking-* |
leading | Multiples of the font size | leading-* |
radius | Points | rounded-* |
shadow, insetShadow, dropShadow | A CSS box-shadow | shadow-*, inset-shadow-*, drop-shadow-* |
blur | Points | blur-*, backdrop-blur-* |
perspective | Points | perspective-* |
aspect | A ratio (1.5) | aspect-* |
ease | [x1, y1, x2, y2] | ease-* |
Unknown keys and invalid values are build errors (nsh.json: tailwind: unknown theme key 'colours'). The theme is compiled into the bundle and applied before the first frame.
The Tailwind theme shapes your classes. Typed methods keep Tailwind’s default scales, so a Rust component looks the same in every app, and take the app’s look from the design tokens of its theme, "theme" in nsh.json (Rust).
#Theme tokens
The app’s theme ("theme" in nsh.json) holds its design tokens: shadcn/ui’s colors (background, foreground, primary, muted-foreground, border, ring, chart-1, sidebar, …), its radius, its fonts font-sans and font-mono, and any token the app adds. Classes name them as Tailwind names the variables of a CSS @theme, Rust components read them with theme() from nsh_base, and TSX reads them with token() from nsh, so a TSX screen and the Rust components follow the same theme:
use nsh_base::theme;
view()
.rounded(move || theme().radius)
.border_1()
.border_color(move || theme().border)
.bg(move || theme().card)
.p_4()
.child(text("Balance").text_color(move || theme().card_foreground))
.child(text("Updated just now").text_color(move || theme().muted_foreground))
.child(
view()
.rounded(move || theme().radius)
.bg(move || theme().primary)
.px_4()
.py_2()
.active(|s| s.bg(move || theme().primary.opacity(0.9)))
.child(text("Continue").text_color(move || theme().primary_foreground)),
)<View class="rounded-(--radius) border border-border bg-card p-4">
<Text class="text-card-foreground">Balance</Text>
<Text class="text-muted-foreground">Updated just now</Text>
<View class="rounded-(--radius) bg-primary px-4 py-2 active:bg-primary/90">
<Text class="text-primary-foreground">Continue</Text>
</View>
</View>A token’s name is its CSS variable’s without --, as the theme file writes it: muted-foreground, radius, chart-1. In Rust, shadcn’s tokens are methods of theme() (primary(), muted_foreground(), radius(), font_mono()), and every token is theme().color("up"), theme().number("radius-card") or theme().font("font-display").
| Form | Takes | Example |
|---|---|---|
| A color name in any color utility | A theme color, with an opacity too | bg-primary, text-muted-foreground/70, bg-up/30, border-t-border, ring-ring, from-accent, placeholder:text-muted-foreground |
(--name), Tailwind 4’s variable shorthand | A theme color or number | bg-(--primary), rounded-(--radius), p-(--gap-card) |
(color:--name), (length:--name), (number:--name), (family-name:--name) | The same, typed where a utility takes several kinds | text-(color:--up), text-(length:--text-title), font-(family-name:--font-mono) |
font-sans, font-mono | The theme’s fonts, when it sets them | font-mono |
- Your own tokens. Every name in the theme file is a token, as every variable of a CSS
@themeis:"up": "#16a34a"makesbg-up,text-upandbg-up/30;"radius-card": "14px"makesrounded-(--radius-card);"font-display": "Pacifico"makesfont-(family-name:--font-display). Lengths are in points;(number:--name)passes the plain number. - Fonts.
font-sansis the app’s font: every text that sets no family of its own is drawn with it, as a page is with Tailwind’s--font-sans.font-monois the font offont-monoand of code inTextView. Unset, both are the system’s fonts. - Both appearances. A token has the light and the dark value of the theme:
bg-primary,theme().primaryandtoken("primary")switch with the appearance by themselves, with nodark:class. A color the theme sets in one appearance only is used in both. - Your Tailwind colors win. A name in
"tailwind"colorsor Tailwind’s palette is that color, sobg-primaryis a theme token when the Tailwind theme has no colorprimary. - The theme can change while the app runs. When an app embedded in a native app is given that app’s theme, and whenever the theme changes, the classes naming tokens, the Rust bindings reading
theme()and the TSX code readingtoken()are updated, the classes and bindings natively. Tokens the embedding app’s theme does not have keep the values of the app’s own"theme"(then the default theme’s). A host that only adds its brand gives theme overrides, the tokens its brand changes in the same file format (iOS, Android,AppView::set_theme_overrideson the desktops). - Checked like other classes. A name that is no token of the default theme or of the app’s
"theme"is an unknown class when building (unknown class 'bg-chart-up'). To use a token only the embedding app provides, give it a value in the app’s"theme"too: that value shows when the app runs on its own.
Classes naming tokens are resolved once per theme, like any other class: drawing them costs what a palette color does.
#token() in TSX
token(name) from nsh is a token’s value in the appearance shown: a color as #rrggbbaa, a length as a number of points, a font as its family name. It is reactive, so code that reads it in a component, a memo or an effect runs again when the theme or the appearance changes, which suits values that go where no class does, such as a canvas or a chart:
import { Canvas, token } from "nsh";
<Canvas
style={{ height: 160 }}
draw={(cx, { width, height }) => {
cx.beginPath();
cx.moveTo(0, height);
cx.lineTo(width, 0);
cx.strokeStyle = token("chart-1");
cx.lineWidth = 2;
cx.stroke();
}}
/>
The names are typed: NSH writes the app’s tokens into .nsh/types/nsh/tokens.d.ts from its theme, so token("up") is a string once "up" is in the theme, and a name the theme lacks is a type error.
#The class check
nsh build, nsh dev, nsh run and nsh test check the static class names in your JSX (string literals in class and the keys of classList) with the same parser and theme the runtime uses, and warn about classes that do not exist:
warn nsh::build App.tsx:12:36: unknown class `bg-brand-550`
Warnings do not fail the build. Classes built at run time (bg-${color()}-500) are checked when the runtime first sees them, which logs each unknown class once (unknown classes: ...). Typed methods are checked by the Rust compiler.
#Classes or styles?
Both are resolved natively and cost about the same. Classes are shorter and give you variants (pressed states, dark mode, breakpoints) without code. Inline styles take values computed in JavaScript and accept native animated values (opacity: fade, translate: [x, 0]). Most TSX apps use classes for the static look and style for what is computed or animated. Rust components use typed methods for both, and bind animated values with .bind_opacity(..), .bind_translate_x(..) and the other bindings.
#Related
- Styling: the style prop, the typed methods and the platform limits of effects.
- Tailwind reference: every utility and variant.
- Layout: flexbox and grid, also available as classes.