.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.
.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.
| Property | Applies to | Values | Effect |
|---|---|---|---|
| Typography | |||
color | text tags + controls + containers | hex / name | text color — set on a container it is inherited by loose text and child text without a rule of its own |
font-size | text tags (h1–h6, p, span, li, Label) + every widget (Button, Input, Select, Tabs, …) & containers | px (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-scale | text tags & containers | integer | legacy size multiplier (font-scale: 2 = font-size: 16px); font-size wins if both are set |
font-family | text tags + controls (body = app default) | family name / :font alias | proportional text in that font |
font-weight / font-style | text tags | bold / italic / normal | bold / italic (b/strong & i/em imply these by default) |
line-height | text tags | integer px | vertical distance between lines |
letter-spacing | text tags | integer px | extra space between characters |
text-align | text tags / Label (classes too: p.centered) | left / center / right | horizontal text alignment |
text-decoration | text tags | underline / line-through | underline or strike-through |
text-transform | text tags | uppercase / lowercase / capitalize | change letter case |
white-space | text tags | nowrap | keep on one line (no wrap; may clip) |
| Box & background | |||
background-color / background | body, containers and controls (Button, Input, Select, Checkbox, Dropdown, Textarea, Switch, Tabs, Slider, RadioGroup, ProgressBar, Modal, Toast, Chart, Tooltip, Image) | hex / name | fill — 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-image | containers | url(path) | image stretched to the box, behind the content, clipped to border-radius |
padding | containers & controls | 1–4 values (CSS sides) | inner spacing — padding: 6 20 = vertical / horizontal |
margin + margin-top/right/bottom/left | any tag / containers | 1–4 values / integer px | outer spacing (shorthand or per-side; the loose sides override the shorthand) |
border / border-color / border-width | containers + controls | 2px solid #333 / hex / px | box outline — follows border-radius (rounded) |
border-radius | containers, controls, Image (by tag / class / id / state) | integer px (or 50%/100% = pill) | antialiased rounded corners — for fills, borders and images |
box-shadow | containers + controls | offX offY blur [spread] color | soft drop shadow behind the box |
opacity | containers | 0–1 | translucent background and text, multiplied down the subtree |
| Sizing & layout | |||
width / height | containers | px or % | explicit box size (50% of the available column) |
min-width / max-width | containers (and body = content column) | px | clamp the box (or the whole content column via body) |
display | any tag / containers | none / grid | hide the element (reactive), or lay children out as a grid |
grid-template-columns / grid-template-rows | display: grid containers | 1fr 1fr, 100px 1fr, repeat(3, 1fr) | column / row tracks — fixed px or fr fractions of the leftover |
gap / column-gap / row-gap | Row, grid & containers | integer px | space between children (grid uses gap for both axes) |
justify-content / align-items | Row | flex-start/center/flex-end/space-between/space-around/space-evenly · flex-start/center/flex-end | distribute / cross-align Row children |
direction | Row | column | lay a Row out vertically |
overflow | containers with height | scroll / hidden | clip children to the fixed-height box; the page flows right below it |
| Positioning & transform | |||
position | children of a container | absolute (+ 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-index | any element | integer | lift the element (and its subtree) above overlapping siblings |
transform | any element | translate(x,y) / translateX/Y · rotate(deg) · scale(s[,s]) / scaleX/Y | visual translate / rotate / scale of the element + subtree (origin = top-left; does not affect the flow) |
| Interactive | |||
cursor | widgets | pointer / 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 */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.
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.
@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 instyleVars(). @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. transformis visual-only (does not affect layout); the origin is the element's top-left, androtate/scaleapply per node (notransform-originyet).- Read the sheet from a
.szsfile, 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.