Documentation menu
serez-ui · Styling

.szs — CSS with logic

serez-ui styles its GUI with .szs: a small CSS dialect with the same selectors and properties you already know, but where conditions are reactive — re-evaluated every frame against your component state and the live window size. It is parsed in pure Serez-Code (no CSS engine), so it stays tiny and predictable.

Part of serez-ui. .szs is not a core built-in — it ships with serez-ui (parsed by parseCss) and styles its GUI renderer. It owns the design (everything visual — color, spacing, alignment, sizing, visibility), while your .szx (JSX) owns the logic and structure — like CSS vs HTML/JS. The editor extension highlights .szs files.

Attach a stylesheet

Load the sheet from a file and hand it to your Window before runGui. Stylesheets live in .szs files (not inline strings): Serez-Code interpolates {} inside string literals, so reading from a file keeps the CSS braces literal. Reading a file needs the File permission.

app.useStylesheet(parseCss(File.read("app.szs")))   // before runGui
app.runGui("App", 640, 480)

Selectors & properties

A rule is selector { property: value; }. The selector is a tag name (body, h1, Button, Row, …), a class (.card, matched against the node's class prop), a comma group (h2, h3 { … }), * (every tag), or a pseudo-state: :hover, :focus, :active and :disabled. A pseudo-state rule can set any property (background, color, border, transform, …), not just a focus ring — Button:hover { background-color: … } works. Focus is opt-in: Input:focus / *:focus (alias :active-focus) turns on the focus mark; without such a rule, focused widgets show no ring. When several rules set the same property, the last matching one wins. Descendant chains (section span) apply under the native renderer.

PropertyApplies toValuesEffect
Typography
colortext tags + controls + containershex / nametext color — set on a container it is inherited by loose text and child text without a rule of its own
font-sizetext tags (h1–h6, p, span, li, Label) + every widget (Button, Input, Select, Tabs, …) & containerspx (e.g. 20px or 20)real pixel font size — any value, not just multiples of 8; a widget grows its box from the label. Inherited down the subtree
font-scaletext tags & containersintegerlegacy size multiplier (font-scale: 2 = font-size: 16px); font-size wins if both are set
font-familytext tags + controls (body = app default)family name / :font aliasproportional text in that font
font-weight / font-styletext tagsbold / italic / normalbold / italic (b/strong & i/em imply these by default)
line-heighttext tagsinteger pxvertical distance between lines
letter-spacingtext tagsinteger pxextra space between characters
text-aligntext tags / Label (classes too: p.centered)left / center / righthorizontal text alignment
text-decorationtext tagsunderline / line-throughunderline or strike-through
text-transformtext tagsuppercase / lowercase / capitalizechange letter case
white-spacetext tagsnowrapkeep on one line (no wrap; may clip)
Box & background
background-color / backgroundbody, containers and controls (Button, Input, Select, Checkbox, Dropdown, Textarea, Switch, Tabs, Slider, RadioGroup, ProgressBar, Modal, Toast, Chart, Tooltip, Image)hex / namefill — on accent-driven widgets it colors the accent (Slider fill/knob, Checkbox box, Switch on-track, RadioGroup dot, Tabs underline), not the whole strip
background-imagecontainersurl(path)image stretched to the box, behind the content, clipped to border-radius
paddingcontainers & controls1–4 values (CSS sides)inner spacing — padding: 6 20 = vertical / horizontal
margin + margin-top/right/bottom/leftany tag / containers1–4 values / integer pxouter spacing (shorthand or per-side; the loose sides override the shorthand)
border / border-color / border-widthcontainers + controls2px solid #333 / hex / pxbox outline — follows border-radius (rounded)
border-radiuscontainers, controls, Image (by tag / class / id / state)integer px (or 50%/100% = pill)antialiased rounded corners — for fills, borders and images
box-shadowcontainers + controlsoffX offY blur [spread] colorsoft drop shadow behind the box
opacitycontainers01translucent background and text, multiplied down the subtree
Sizing & layout
width / heightcontainerspx or %explicit box size (50% of the available column)
min-width / max-widthcontainers (and body = content column)pxclamp the box (or the whole content column via body)
displayany tag / containersnone / gridhide the element (reactive), or lay children out as a grid
grid-template-columns / grid-template-rowsdisplay: grid containers1fr 1fr, 100px 1fr, repeat(3, 1fr)column / row tracks — fixed px or fr fractions of the leftover
gap / column-gap / row-gapRow, grid & containersinteger pxspace between children (grid uses gap for both axes)
justify-content / align-itemsRowflex-start/center/flex-end/space-between/space-around/space-evenly · flex-start/center/flex-enddistribute / cross-align Row children
directionRowcolumnlay a Row out vertically
overflowcontainers with heightscroll / hiddenclip children to the fixed-height box; the page flows right below it
Positioning & transform
positionchildren of a containerabsolute (+ left/right/top) · relative (+ top/left offset)absolute = out-of-flow badge anchored to the parent box; relative = shift the element visually without moving the flow
z-indexany elementintegerlift the element (and its subtree) above overlapping siblings
transformany elementtranslate(x,y) / translateX/Y · rotate(deg) · scale(s[,s]) / scaleX/Yvisual translate / rotate / scale of the element + subtree (origin = top-left; does not affect the flow)
Interactive
cursorwidgetspointer / text / move / crosshair / not-allowed / …mouse cursor while hovering

Colors are hex (#rgb, #rrggbb, and the color-picker forms #rgba / #rrggbbaa — the alpha channel is ignored by the interpreted renderer; under the native renderer it makes the background truly translucent, like rgba()), rgb(r, g, b) / rgba(r, g, b, a), or a basic name (white black red green blue yellow gray). Sizes are pixels — the px unit is optional (20px and 20 are the same).

/* app.szs */
body   { background-color: #0f172a; }
h1     { color: #ffd166; font-scale: 3; }
p      { color: #94a3b8; }
Button { background-color: #2563eb; color: #ffffff; }

.card  { background-color: #1e293b; border: 1px solid #334155; border-radius: 8; padding: 12; }
Input:focus { border-color: #22d3ee; }   /* opt-in focus mark */
Not full CSS. The renderer reads the properties above today. The parser itself accepts any property: value, so unknown ones are simply ignored — more get wired to the renderer as serez-ui grows (see Looking ahead).

Fonts — :font and font-family

Declare font files in a :font block (the renderer loads them lazily) and pick families per tag with font-family. System-installed fonts work by name without any block; a downloaded .ttf (e.g. a Google Font) goes through :font. Set it on body and the whole app inherits it.

:font {
    Titular: fonts/Roboto.ttf;        /* alias: path to a .ttf/.otf */
}

body { font-family: "Segoe UI"; }     /* app-wide default (system font) */
h1   { font-family: "Titular"; }      /* the :font alias */

Custom families render proportionally (real glyph advances, antialiased) and text wrapping measures with the actual font. Input / Textarea content stays monospace so caret positioning holds.

Reactive conditions — the logic

Any selector can carry a condition in parentheses, re-checked every frame: selector (var OP literal) { … }. Operators: == != < > <= >= — or a bare variable (.warn (alert) { … }) which applies while the value is truthy (false, 0, "" and nulldon't pass). If the variable is not available, the rule simply does not apply.

body (count == 0) { background-color: #0f172a; }
body (count != 0) { background-color: #14532d; }
.warn (alert)     { background-color: #7f1d1d; }   /* bare boolean */

The variables come from two places: your component state (via styleVars()) and the built-in width / height (the live window size, in px).

Combining conditions — and / or / not

Conditions compose with and, or and not, spelled as words like in a CSS media query. The symbolic && / || / ! are accepted as aliases, so both styles work.

body  (width > 600 and flag == true) { background-color: #c12; }
.item (selected or hovered)          { border-color: #3b82f6; }
.row  (not hidden)                   { display: flex; }

/* same three, with the symbolic aliases */
body  (width > 600 && flag == true)  { background-color: #c12; }
.item (selected || hovered)          { border-color: #3b82f6; }
.row  (!hidden)                      { display: flex; }

Precedence is the usual one: not binds tighter than and, and and tighter than or — so a or b and c reads as a or (b and c). There are no grouping parentheses: the sheet scanner closes the condition at the first ), so if you need an explicit grouping, precompute it as a single variable in styleVars().

The connectives only count as whole words and quoted values are respected, so a variable named android or notify, or a value like "a or b", are never split.

Native renderer: under useNativeRenderer(true)the sheet is handed to the core's own CSS engine, so composite conditions need a core new enough to understand them — on an older core those rules simply do not apply. The default interpreted renderer works on any supported core.

Grouping many rules — @when / @else

Wrap several rules in a @when (condition) block to share one condition across many selectors (tags, classes, ids) — instead of repeating it rule by rule. The condition is the same reactive logic as above (a styleVars() variable, or width / height, with and / or / not) — not a fixed media query.

@when (width < 300 and darkMode) {
    body   { color: #fff }
    .card  { padding: 8 }
    #main  { gap: 4 }
}

@else is the complement of the preceding @when, and @else (condition) chains an else-if. The branches are mutually exclusive — evaluated top to bottom, the first match wins — so you never have to negate a range by hand:

@when (width < 200) { body { color: #100 } }
@else (width < 400) { body { color: #200 } }
@else               { body { color: #300 } }

Blocks nest (a @when inside a @when — the conditions are AND-ed), and a rule inside a block may still carry its own (condition), combined with the block's. @else negates the whole preceding condition, so a composite like (a or b) complements correctly. Unknown at-rules (@media, …) are ignored.

Still order-based. A @when only groups the condition — it does not add specificity. The last matching rule wins for a given property, so if a block lower in the sheet also sets it, that later value takes over regardless of the @when nesting. Put the block that should win last.

Exposing state — styleVars()

Override styleVars() to publish the state your conditions read. (An optional :import { … } block at the top of the sheet documents which variables you use, but the values are taken from styleVars() / width / height directly.)

public any styleVars() { return [["count", this.count]] }

Responsive — media queries

width and height (px) are always available — no styleVars() needed — so .szs doubles as a responsive system, and it reflows live as the window resizes:

body (width < 600)  { background-color: #1e1b4b; }   /* phone-ish */
body (width >= 600) { background-color: #0f172a; }

Row (width < 600)   { direction: column; }            /* stack the row when narrow */

h1  (width < 600)   { font-scale: 2; }
h1  (width >= 960)  { font-scale: 4; }

Pair this with code-level breakpoints (app.breakpoint()) and automatic Row wrapping — see the serez-ui responsive section.

A complete example

/* counter.szs */
body (count == 0) { background-color: #0f172a; }
body (count != 0) { background-color: #14532d; }

h1 (width < 600)  { font-scale: 2; }
h1 (width >= 600) { font-scale: 3; }
h1 { color: #ffd166; }

Button { background-color: #2563eb; color: #ffffff; }
// counter.szx
class Counter:Window {
    public Counter() { super(); this.count = 0 }

    public render() {
        return (
            <div>
                <h1>Count: {this.count}</h1>
                <Button onClick={() => { this.count = this.count + 1 }}>+1</Button>
            </div>
        )
    }

    // expose state to the .szs conditions
    public any styleVars() { return [["count", this.count]] }
}

let app = new Counter()
app.useStylesheet(parseCss(File.read("counter.szs")))
app.runGui("Counter", 520, 360)

Notes & limits

  • Conditions combine with and / or / not, but there are no grouping parentheses — precompute an explicit grouping as one variable in styleVars().
  • @when (condition) { … } groups many rules under one shared condition; @else / @else (condition) chain mutually-exclusive branches. They group the condition only — the cascade is still last-match-wins.
  • Properties are limited to the table above; other declarations parse but are ignored.
  • Last matching rule wins for a given property (no specificity ranking).
  • Pseudo-states (:hover / :focus / :active / :disabled) work in both renderers and accept any property. Descendant selectors (section span) only apply under the native renderer.
  • transform is visual-only (does not affect layout); the origin is the element's top-left, and rotate/scale apply per node (no transform-origin yet).
  • Read the sheet from a .szs file, not an inline string.
  • Colors: hex (#rgb / #rrggbb / #rgba / #rrggbbaa), rgb() / rgba(), or a basic name. Alpha tints the background only under the native renderer; the interpreted one reads the RGB and ignores it.

Looking ahead

The table above now covers most day-to-day CSS: full typography (including real font-size in px and letter-spacing), the box model, flex Row and grid, positioning, transform, and interactive pseudo-states. Still on the list: grid-template-areas, background-size: cover/contain, a configurable transform-origin, and niche pieces (box-sizing, outline, filter, transition/animation). The parser accepts arbitrary property: value declarations, so unknown ones are ignored and new ones get wired without a syntax change — this page tracks the surface as it expands.

Back to the serez-ui reference (all components + responsive), or follow the Build a UI guide.