Honored CSS
The authoritative list of properties the runtime interprets, plus selectors, pseudo-classes and transitions.
display (block|flex|grid|inline|none)
flex-direction, justify-content, align-items, gap, row-gap, column-gap
align-self, justify-self, justify-items, align-content
flex-grow, flex-shrink, flex-basis, flex-wrap, flex (shorthand)
grid-template-columns, grid-template-rows
grid-column, grid-column-start, grid-column-end
grid-row, grid-row-start, grid-row-end (1 / 3, span 2, -1; no named lines)
grid-auto-flow, grid-auto-rows, grid-auto-columns
transform (translate/scale/rotate; visual only; hit regions aren't transformed)
position (static|relative|sticky|absolute|fixed), top, right, bottom, left
aspect-ratio
width, height, min-width, max-width, min-height, max-height
padding, padding-top, padding-right, padding-bottom, padding-left
margin, margin-top, margin-right, margin-bottom, margin-left
(each shorthand takes 1–4 values)
border, border-width, border-color
border-top, border-right, border-bottom, border-left
border-top-width, border-right-width, border-bottom-width, border-left-width
background, background-color, background-image, opacity
(colour, linear-/radial-gradient, or url(…) image, cover-sized, clipped to corners)
box-shadow (single, outer; inset parsed but not drawn)
transition (property duration easing delay, comma-separated; see below)
border-radius (1–4 diagonal shorthand; px/rem/em, and % against the box)
border-top-left-radius, border-top-right-radius
border-bottom-right-radius, border-bottom-left-radius
color, font-size, font-weight, font-family, font-style (italic), text-align
letter-spacing, word-spacing, line-height, white-space (nowrap|pre)
text-decoration, text-decoration-line (underline / line-through; the longhand
wins where both are set) (color: hex, rgb()/rgba(), CSS names)
overflow, overflow-x, overflow-y (hidden|clip = clip; auto|scroll = scroll;
both axes together; x and y can't differ)
overflow-wrap (break-word), word-wrap (the legacy alias for it), word-break (break-all)
cursor (pointer, on @tap boxes only)
fill, fill-rule, stroke, stroke-width, stroke-linecap, stroke-linejoin
(<path> only; see above)
Percentages need a box, and most of these are resolved before there is one.
width, height, min/max-* and the insets become a length that layout
resolves, so width: 50% works. padding, margin, gap/row-gap/
column-gap, the border widths, font-size, letter-spacing and
word-spacing are resolved to plain pixels during the cascade, where there is
no box yet, so a percentage on one of those is ignored and says so. Until
v0.7.1 it was ignored in silence, because the interpreter read them with a
px-only parser while the length check validated with a percentage-capable one:
the value was dropped and then pronounced fine. Real percentage support for them
is scheduled.
A border may differ per side. border-bottom: 2px solid #89b4fa draws under
the box and nowhere else, which is the underlined-field shape most forms want.
Until v0.7.1 it drew nothing: the cascade computed all four sides, and paint
carried one width taken from the top, so border-bottom was dropped and
border-top was drawn on all four sides. Both silent, and border-bottom is
offered by the editor's completion list, which is supposed to mean it works.
Uniform borders are drawn as one stroke, which is what follows the corner radius. Uneven ones are drawn as four filled edges, so where two different non-zero widths meet the corner is square rather than mitred diagonally. That difference is only visible on a box that sets two adjacent sides to different widths.
border-radius is the exception, and takes a percentage. It is the one of
these that layout never reads: only paint does, and paint knows the box. So
border-radius: 50% is resolved against the laid-out box and a square comes out
a circle. It works on the corner longhands too, and a longhand in px after a
shorthand in % replaces that corner in both units.
A percentage resolves against the shorter side, not per axis. Rux draws
circular corners (one radius per corner), so 50% on a 160x60 box is a pill of
radius 30 rather than the ellipse CSS would draw. On a square the two agree
exactly, and the pill is what someone writing 50% on a button is after.
border-radius: 9999px still works and still means "as round as it goes", since
a radius larger than the box is clamped to it. That is what the runtime itself
uses to draw a radio button.
Selectors: tag, .class, #id, [role="…"], compounds, and all four
combinators: descendant (.a .b), child (.a > .b), next-sibling (.a + .b),
subsequent-sibling (.a ~ .b).
Pseudo-classes: :hover, :focus, :active, :checked, :current (a
link whose to names the path you are on), and :enter-from / :leave-to
(the two sides of an enter/leave swap, below). They stack
(.btn:hover:active), count as class-level specificity, and work anywhere in a
chain, .card:hover .title recolours the title while the pointer is over the
card. :hover/:active hold for the whole chain under the pointer, as in CSS;
:active is press-to-release and drops if you drag off the element; :focus
matches the input holding the caret. Driven in examples/pseudo.rux.
Any other pseudo-class (:disabled, :nth-child(…), ::selection) never
matches, and says so once on stderr. Before this existed the : was silently
dropped, so .box:hover parsed as .box and applied unconditionally, failing
closed is the safer half of that trade.
Transitions: transition walks a property to its new value instead of
jumping to it, whatever moved it: a signal, a :class, or a pseudo-class.
.card { transition: background-color 200ms ease-out, transform 200ms ease-out; }
.card:hover { background: #45475a; transform: translateY(-4px); }
.panel { height: 0; opacity: 0; transition: all 250ms ease-in-out 50ms; }
.panel.open { height: 60px; opacity: 1; }
Each entry is a property, a duration, an easing and a delay, in any order after
the property. As in CSS the first time is the duration and the second is the
delay, all stands for every animatable property, and a bare transition: 200ms
means all 200ms. Easings: linear, ease (the default), ease-in,
ease-out, ease-in-out, cubic-bezier(x1, y1, x2, y2). steps() is not
supported.
Animatable: opacity, background-color, color, border-color,
border-width, border-radius, width, height, padding, margin, gap,
font-size, transform, the insets (top/right/bottom/left, which
animate together), and on a <path>: fill, stroke, stroke-width and d,
the geometry itself. Naming anything else warns and lists what is animatable,
rather than leaving you with an element that silently never moves. A longhand of
an animatable shorthand (padding-left) is pointed at the shorthand: the four
sides animate as a unit.
Three limits, all of them deliberate:
- A value that has no midpoint jumps.
10px→50%needs a layout to resolve, and a colour becoming a gradient has no halfway. Same-unit lengths interpolate; anything else lands at once. - Enter and leave are opt-in. Unmarked, a node still arrives at its
authored style and a node the build stops reaching is simply gone. Add
r-transitionto animate the way in and out; see below. transitiondoes not inherit, exactly as in CSS. A parent'scolorchange does not animate a child's text; put the transition on the element whose style is moving.
An app with no transitions running is still fully event-driven: it sleeps
waiting for real events and renders nothing. Frames are scheduled only while
something is actually in flight, and stop the frame it lands. Driven in
examples/transition.rux.
Enter and leave: r-transition on an element with r-if (or on a keyed
r-for row) says that its arrival and departure are animated. What the two
sides look like is CSS, on :enter-from and :leave-to; how long the swap
lasts is the element's own transition.
<view class="panel" r-if="open" r-transition>…</view>.panel { opacity: 1; transform: translateY(0px);
transition: opacity 300ms ease-out, transform 300ms ease-out; }
.panel:enter-from { opacity: 0; transform: translateY(-16px); }
.panel:leave-to { opacity: 0; transform: translateY(-16px); }
The two sides are ordinary rules and the walk between them is the same
machinery a :hover uses. :enter-from is worn for one frame and dropped by
the next, which is what turns an arrival into an ordinary style change;
:leave-to is held from the moment the swap opens until it commits.
While a swap is pending both branches are really there: laid out, styled,
and still updating. That is why the condition changing back mid-swap reverses
the swap instead of stacking a second one, and why a departing component's
unmounted fires when the swap commits rather than when it starts. A
cancelled swap never fired one.