CSS & Styling

Style your UI with CSS, the same way you’d style a web page. Cuttlefish understands a practical subset of CSS — the box model, flexbox, colors, typography, and selectors. All of it is resolved when you build (no CSS engine runs on the device): flexbox is laid out with Yoga (a layout library), styles are resolved, and the resulting draw commands are what the firmware runs.

Box model

PropertyValuesNotes
padding8px, 8px 16pxShorthand supported
margin8px, 8px 16pxShorthand supported
width / height100px, 50pxExplicit size
min-width / max-width / min-height / max-height100pxYoga constraints
aspect-ratio16 / 9, 1 / 1, 1.5Infers the missing dimension
box-sizingborder-boxYoga border-box
overflowhidden, scrollBoth values make the container a touch-drag scroll viewport

Length units

px, bare numbers, and rem/em (multiplied by 16, the root font size — so 0.625rem = 10px). Percentages are honored where applicable. calc() evaluates + - * / on lengths after var() substitution, with operator precedence.

Flexbox (via Yoga)

PropertyValues
displayflex, none
flex-directionrow, row-reverse, column, column-reverse
gap8px (sets both row and column gap)
row-gap / column-gap8px (per-axis; overrides uniform gap)
flex-grow1
flex-shrink0
flex-basisauto, 120px, 50%
flex (shorthand)1, 1 0 auto, none
align-itemsflex-start, center, flex-end, stretch
align-selfflex-start, center, flex-end, stretch, baseline
align-contentflex-start, center, flex-end, stretch, space-between, space-around, space-evenly
justify-contentflex-start, center, flex-end, space-between, space-around, space-evenly
flex-wrapwrap, nowrap, wrap-reverse
order1, 2, …
positionrelative, absolute, static
top / right / bottom / left10px
z-indexnumeric layers (inherited by descendants)

display: none removes the element subtree from layout, drawing, and hit-testing while preserving generated node indices.

Colors

All standard CSS formats: #rrggbb, #rgb, #rrggbbaa (alpha ignored), rgb(r,g,b), rgba(r,g,b,a) (alpha ignored), hsl(...), hsla(...). Both comma and CSS4 space syntaxes work. The 147 CSS named colors (red, dodgerblue, transparent, …) are supported.

Typography

PropertyValuesNotes
colorany colorText foreground
font-family"MyFont"Uses a generated font when matched by @font-face; otherwise the built-in bitmap font
font-size16pxGenerated fonts rasterize at this pixel size
fontitalic bold 18px DeviceSansShorthand for style/weight/size/family
text-alignleft, center, right
text-decorationunderline, line-through, noneCombine: underline line-through
text-overflowellipsis, clipTruncates overflowing single-line text
text-transformuppercase, lowercase, capitalize, noneApplied at transpile time
line-height1.5, 150%, 24pxnormal = font default
letter-spacing2px, -1px
white-spacenormal, nowrap, pre, pre-line
font-weightnormal, bold, 400, 700Selects the matching @font-face variant
font-stylenormal, italic, oblique
font-smoothinganti aliased, noneOverrides display-level antialiasing
font-subsetexact, fallbackControls glyph selection for generated fonts

Fonts (@font-face)

Declare variants with @font-face. When you build, Cuttlefish keeps only the font characters you actually use:

@font-face {
  font-family: "DeviceSans";
  src: url("./assets/DeviceSans-Bold.ttf");
  font-weight: bold;
}
  • font-subset: exact — include only the exact glyphs used (smallest binary).
  • font-subset: fallback — include fallback glyphs for dynamic text that may contain unanticipated characters.
  • font-smoothing: antialiased — enables per-pixel alpha for the matched text.

Visual properties

PropertyValuesNotes
background / background-colorany color, linear-gradient(…)Two-stop linear gradients: background: linear-gradient(to bottom, #111, #333)
border (shorthand)2px solid #808080
border-width / border-color / border-style2px / color / solid dashed dotted double noneDashed is approximated with segments
border-left / border-top / border-right / border-bottom2px solid #808080Per-side borders
border-radius4pxUniform only (no per-corner)
outline1px solid #fffDrawn outside the element box
visibilityvisible, hidden
box-shadowinset 0 1px 0 #fff, 0 10px 0 #333Up to 4 rect shadows; approximated
transformtranslateY(10px), translate(0, 10px), scale(1.1), scaleX(0.9), rotate(15deg)Draw-time offset/transform; no flex relayout. Percent-based translate resolves against the element’s own size
transform-origincenter, top left, 50% 100%Anchor for scale/rotate
opacitynumberBlended toward the parent/background color (approximate — no framebuffer needed)

Transitions, animations, and :pressed

transition: background 300ms;
transition: color 120ms;

Smoothly transitions the property over the duration. The :pressed pseudo-class applies while a button’s .value is 1:

#ok { background: #2563eb; transition: background 120ms; }
#ok:pressed { background: #1e40af; transform: translateY(2px); }

:pressed rules may include top/left/right/bottom or transform: translate(...) as draw-time offsets (outset shadows stay anchored).

@keyframes animations

Define keyframe sets and attach them with the animation property (or its long forms animation-name, -duration, -iteration-count, -delay, -timing-function):

@keyframes pulse {
  from { opacity: 1; }
  to   { opacity: 0.4; }
}

#indicator { animation: pulse 1.2s infinite ease-in-out; }

Animatable properties are background, color, opacity, transform, and size (width/height). The shorthand accepts name duration iterations delay timing in any order; infinite loops forever, and linear/ease/ease-in/ease-out/ease-in-out shape the interpolation. direction (normal, reverse, alternate, alternate-reverse), fill-mode, and running/paused tokens are parsed for compatibility. Animations advance in ui_tick() alongside transitions.

Selectors

KindExample
Elementscreen
ID#title
Class.card
Compound.card.active, button.primary
Descendantview text
Childview > text
Adjacent sibling.first + .second
General sibling.first ~ .later
Attribute[disabled], [type="number"]
Negationbutton:not(.disabled), .a:not(.b.c)
Pseudo-state:pressed, :active, :disabled, :checked, :focus

Pseudo-states: :pressed (button held; :active is an alias), :disabled, :checked (check/radio .value is 1), :focus.

Inline style="…" attributes and <style> blocks are both supported.

CSS variables and theming

Define variables in :root and reference with var():

:root {
  --bg: #111;
  --fg: #eee;
}
screen { background: var(--bg); color: var(--fg); }

Class-scoped themes select a variant at build time via themeClass in your cuttlefish.config.ts:

:root { --bg: #fff; --fg: #000; }
.dark { --bg: #111; --fg: #eee; }
display: { themeClass: 'dark', /* … */ }

The built-in component kit

A shadcn-style component kit is built into @typecad/ui and always included — its tokens and class recipes are prepended ahead of your stylesheets, so anything you write overrides them by normal cascade order. No imports or scaffolding: put the classes on native elements and they work.

<button class="btn btn-primary">Save</button>
<view class="card">
  <view class="card-header">
    <text class="card-title">Title</text>
    <text class="card-description">Subtitle</text>
  </view>
  <view class="card-content"> ... </view>
  <view class="card-footer"> ... </view>
</view>

The kit covers btn (variants -secondary -outline -ghost -destructive, sizes .btn-sm/.btn-lg, .btn-block), badge, card, input, form-label, row, separator/vseparator, alert, skeleton, spinner, dialog and toast recipes (dialog-scrim, dialog-card, dialog-footer, toast, toast-title, toast-description), progress, avatar, switch (a styled <check>), tabs, and accordion. Validation states (.input-error, .field-error) are runtime-driven — bind borderColor/visible to a signal.

The kit’s design tokens are ordinary CSS variables (--primary, --background, --card, --muted, --destructive, --border, --radius, …), themed through the checked pseudo-state as well: :checked rules resolve a --primary/--primary-foreground pair, so switches, checkboxes, radios, and select option rows recolor automatically per theme.

Themes

A theme is any CSS file. Pre-packaged ones ship with @typecad/ui@import one by bare specifier from a <style> block (or the sidecar .ui.css file):

@import "@typecad/ui/themes/blue.css";

Available themes: zinc, slate, stone, gray, neutral, blue, green, red. Dark mode activates with themeClass: 'dark' in the config’s display block. To use your own, save a ui.shadcn.com / tweakcn export into the project and @import it by path — its :root/.dark token blocks override the kit defaults (later definitions win). Both dialects parse: classic HSL channel triplets and Tailwind-v4 oklch(). Alpha in colors is ignored (no blending on bare metal). Redefine any kit class in your own stylesheet and your definition wins.

@media performs compile-time variant selection (no runtime resizing). Supported conditions: min-width, max-width, min-height, max-height (in px), exact-match (width: N)/(height: N), and the feature queries (e-ink), (update: slow|fast), (monochrome) / (monochrome: N), and (color-gamut: srgb|p3). orientation is unsupported (warned and skipped).

@media (max-width: 200px) {
  screen { flex-direction: column; }
}

Unsupported (and why)

These are intentionally out of scope for microcontroller rendering:

  • display: grid — use flexbox
  • background-image / CSS sprites — use <img> instead, or a two-stop linear-gradient() on background
  • position: fixed
  • ::before / ::after pseudo-elements
  • Per-corner border-radius (uniform only)
  • Runtime theme switching (themes are resolved at transpile time)
  • @supports (@import is supported for theme files — see Themes)

Inline rich text (mixed bold/italic/underline spans via <b>/<i>/<u>/<a>, line breaks with <br>) and text-shadow are supported — see Elements.

Next: Display Configuration for wiring a physical display.