StyleX Styling
This project uses StyleX for all styling. The system has three layers: design tokens for values, design primitives for multi-property patterns, and stylex.create for component-specific styles. All styles are applied via a custom css prop.
The import paths below are the @tuja/ui package exports that apps/web uses. Inside packages/ui, import the same files by relative path, such as ../../tokens.stylex.ts from a component.
Quick Decision Guide
| Need | Use | Example |
| ------------------------------------------------------ | ------------------------------ | ----------------------------------------------- |
| Flex layout, fills, truncation, resets, transitions | Design primitives | css={flex.row} |
| Rounded corners | Design primitives (corner.*) | css={corner.radius_3} |
| Override a primitive's default | Layout modifier | css={[flex.row, align.end]} |
| Single-property styling (color, spacing, font, border) | stylex.create + tokens | color: color.fg |
| Responsive behavior | stylex.create + breakpoints | { default: "none", [breakpoints.md]: "flex" } |
| Pseudo-selectors (hover, focus) | stylex.create | { default: val, ":hover": hoverVal } |
The css Prop
Use css={styles.foo} instead of {...stylex.props(styles.foo)}. This is StyleX's official JSX shorthand (sx), configured under the name css via the sxPropName Babel option.
// Single style
<div css={styles.card}>
// Composed — array of styles, primitives, and conditionals
<div css={[flex.row, styles.header, isActive && styles.active]}>
The transform only compiles css on lowercase host elements (div, svg, …). On a component, css is a real runtime prop carrying raw StyleX styles:
- A component that should take styles declares
css?: StyleProp(from@tuja/ui/types, orsrc/types.tsby relative path insidepackages/ui) and composes it last into its root element'scssarray:css={[styles.base, css]}. Every@tuja/uicomponent works this way —cssis the only styling entry; components do not acceptclassNameorstyle. - NEVER pass
cssto a third-party component (next/link, next/image, Phosphor icons) — it doesn't know the prop. Spread compiled props instead:<Link {...stylex.props(styles.cta)}>. - NEVER put an explicit
className=/style=attribute on the same host element ascss=— the compiled spread and the attributes clobber each other, and merging is never needed:- A runtime-computed value belongs in a dynamic style function, not a
styleattribute:stylex.create({ swatch: (bg: string) => ({ backgroundColor: bg }) }), applied ascss={[styles.tone, styles.swatch(hex)]}. Custom properties work too:(x: string) => ({ "--nudge-x": x }). - A literal class required by a third-party stylesheet (the repo has exactly one: LyteNyte's
ln-gridin media-table.tsx) is concatenated inline:const sx = stylex.props(...); <div {...sx} className={${sx.className ?? ""} ln-grid}>.
- A runtime-computed value belongs in a dynamic style function, not a
Design Tokens
Import from @tuja/ui/tokens.stylex. All tokens are theme-aware. For the full catalog of every token and its values, read references/tokens.md.
Categories: color, space, controlSize, font, border, shadow, layer, opacity, ratio, plus the constants and layout consts.
import { color, space, border, font } from "@tuja/ui/tokens.stylex";
const styles = stylex.create({
card: {
padding: space._4,
borderWidth: border.size_1,
backgroundColor: color.bgSurfaceRaised,
fontSize: font.uiBody,
},
});
Rounded corners are the one exception: don't reach for a bare border.radius_* here — use the corner primitive below instead, so the radius always ships paired with its corner shape.
Breakpoints
Import from @tuja/ui/breakpoints.stylex. Values: sm (320px), md (768px), lg (1080px), xl (2000px).
import { breakpoints } from "@tuja/ui/breakpoints.stylex";
const styles = stylex.create({
grid: {
display: { default: "none", [breakpoints.md]: "grid" },
gridTemplateColumns: { default: "1fr", [breakpoints.lg]: "repeat(3, 1fr)" },
},
});
Design Primitives
Composable multi-property styles in packages/ui/src/primitives/. Each primitive bundles 2+ CSS properties that encode a common pattern. For full API tables, read references/primitives.md.
Flex (@tuja/ui/primitives/flex.stylex)
The most commonly used primitives. Flex patterns set display: flex plus layout defaults:
flex.row— horizontal, vertically centeredflex.col— vertical stackflex.center— centered both axesflex.between— space-between with vertical centeringflex.wrap— wrapping rowflex.inlineCenter— inline-flex centered
Override defaults with modifiers: align.{start,center,end,baseline,stretch}, justify.{start,center,end,between}, grow.{_0,_1}, shrink.{_0,_1}.
import { flex, align, justify } from "@tuja/ui/primitives/flex.stylex";
<div css={flex.row}> {/* basic row */}
<div css={[flex.row, align.end]}> {/* row, bottom-aligned */}
<header css={flex.between}> {/* toolbar pattern */}
<div css={[flex.col, justify.center]}> {/* vertically centered column */}
Corner (@tuja/ui/primitives/corner.stylex)
Pairs each border.radius_* step with its corner shape in one declaration — squircle on corner.radius_1 … corner.radius_5, circular caps on corner.radius_round (clamped into a pill or a circle, a superellipse cap reads as neither). corner.squircle_round keeps the squircle shape at that same full-round radius, closing at half the cornerTokens.height dial — the shape Button and SegmentedControl use, each overriding the dial to their own control height. Rounded corners always go through this primitive; never write a bare borderRadius.
import { corner } from "@tuja/ui/primitives/corner.stylex";
<div css={corner.radius_3}> {/* card corner */}
<span css={corner.radius_round}> {/* pill / avatar */}
<button css={corner.squircle_round}> {/* squircle pill, dialed via cornerTokens.height */}
If a radius genuinely can't go through the primitive — a vendor pseudo-element, a CSS-var-driven radius — pair cornerShape beside borderRadius in the same object literal instead ("squircle", or "round" at the full-round radius). The @tuja/require-corner-shape ESLint rule enforces this in packages/ui and apps/web.
There is no global corner-shape rule anywhere — every rounded corner carries its own shape through the primitive or a local cornerShape pairing.
Material (@tuja/ui/primitives/texture.stylex, @tuja/ui/primitives/wash.stylex)
Faint surface treatments — Texture, Wash, and Glass; the rules are on the texture and wash showcases (apps/web/src/design-system/sections/foundations/texture-showcase.tsx, wash-showcase.tsx). texture.dot draws one dot of 1px or less, repeated across a surface at one size — never nest a textured surface inside another, and never mix two sizes in one group. wash.toBottom/toTop/toRight/toLeft are a gradient of one tone fading to transparent — a Wash has no bright spot; a bright spot reads as a light source, and only Glass is lit.
Each dials its default through a token, overridden in a local stylex.create the way cornerTokens.height is:
import { texture, textureTokens } from "@tuja/ui/primitives/texture.stylex";
import { space } from "@tuja/ui/tokens.stylex";
const styles = stylex.create({ wide: { [textureTokens.pitch]: space._4 } });
<div css={[texture.dot, styles.wide]}> {/* wider pitch for a wide surface */}
textureTokens.pitch/.ink default to space._1/color.fg at 20%; washTokens.tone defaults to color.bgNeutralSubtle.
Glass is the third Material but ships as a component style object, not a primitive: glassSurface from @tuja/ui/components/glass-surface.stylex, composed onto an element with position: relative plus a corner.* preset. glassTokens (fill, border, highlight, blur) is its dial, overridden the same way.
Other Primitives (see references/primitives.md)
- Layout — position fills, scroll containers, truncation, image fit
- Reset —
buttonReset.basestrips browser button chrome - Motion — transition/animation presets with reduced-motion handling
- A11y —
srOnlyvisually hides text while keeping it announced;focusRing/focusRingInsetpaint the keyboard focus ring
Best Practices
- Primitives for multi-property patterns — flex, fills, truncation, resets, transitions
- Tokens for single properties —
fontSize: font.uiBody,gap: space._3 - Rounded corners via
corner.*, never a bareborderRadius— paircornerShapelocally only where the primitive can't reach - Always use the
cssprop — never{...stylex.props()} - Conditional styles via arrays —
css={[base, condition && conditional]} - Mobile-first — use breakpoint overrides for larger screens
- Theme-aware colors — use
colortokens that adapt to light/dark - Logical properties — prefer
paddingBlock/paddingInlineover directional - Pseudo-selectors as object keys —
{ default: val, ":hover": hoverVal }