CSS Properties

ReactUnity allows you to style elements using CSS. The CSS properties works the same as in browsers unless specified otherwise.

:root {
  justify-content: space-around;
  text-align: center;
  background-color: #a9b2ba;
}

view {
  align-self: center;
  width: 100px;
  height: 100px;
  background-color: coral;
  cursor: pointer;
}

view:hover {
  background-color: blue;
}

CSS Properties

List of supported CSS properties in alphabetical order:

CSS Shorthands

List of supported CSS shorthands in alphabetical order:

Math functions

calc(), min(), max() and clamp() are joined by round() with its nearest, up, down and to-zero strategies, mod(), rem(), abs(), sign(), sin(), cos(), tan(), asin(), acos(), atan(), atan2(), pow(), sqrt(), hypot(), log() and exp(). They nest freely inside one another and inside calc(), and take var() operands. The constants pi, e, infinity, -infinity and NaN are known.

view {
  animation-duration: round(up, 1250ms, 1s);
  opacity: calc(0.5 + 0.5 * sin(var(--phase)));
  flex-grow: calc(pow(2, var(--level)) - 1);
}

A calculation on a layout value — width, left, flex-basis, translate and the rest — has to come out as something Yoga can hold, which is a length or a percentage but never both. width: calc(1/2 * 100%) is 50%, so every fraction utility a CSS framework emits lays out; width: calc(100% - 2rem) has no unit to resolve to and is dropped. The percentage survives a sub-expression, parenthesised or a nested calc(), so calc(calc(1 / 2 * 100%) * -1) — the shape a negative fraction utility compiles to — is -50%. A min(), max() or clamp() whose arguments are all percentages is a percentage too; one that mixes a percentage with a length resolves the percentage against the box, which only the sizing properties and the border widths have an answer for. infinity is the largest length the implementation supports rather than a real infinity, which is what makes border-radius: calc(infinity * 1px) a pill.

place-content, place-items and place-self set the align and justify pair in one declaration. Yoga has no justify-items or justify-self, so the last two set their align property and only check that the second value is a keyword. The web spellings start, end, self-start, self-end, left, right and normal work on the align and justify longhands too.

Display and the flex defaults

Every element lays out with flexbox, and display picks the defaults a browser would give it. An element with no display, or with display: block, stacks its children the way a block does, and display: flex lays them out in a row. Either way flex-direction, gap, align-items and the rest work on it. inline, inline-block and flow-root read as block, inline-flex as flex, and none and contents do what they do on the web. There is no inline formatting for the inline spellings to differ in, so each is the block value it reduces to rather than a dropped declaration.

flex-shrink defaults to 1 as on the web, so an item gives up space before it overflows, and the flex shorthand fills in the web’s values: flex: 1 is 1 1 0%, flex: auto is 1 1 auto and flex: none is 0 0 auto.

Logical properties

The inline axis is the one direction turns around, and the block axis is always vertical — writing-mode does not exist here, so block-start is the top and block-end the bottom whatever else is going on.

view {
  padding-inline: 4px 12px;
  padding-block-start: 8px;
}

direction is inherited, so setting it once turns the inline axis around for a whole subtree:

#panel {
  direction: rtl;
}

Which physical side each inline value lands on is decided from the direction the element ends up with — an element does not need a direction of its own to follow one set on an ancestor. For everything Yoga lays out that decision is Yoga’s, made during layout; the painted border properties below are resolved by ReactUnity itself, by the same rule. An inline value takes precedence over the physical property covering the same edge, whichever order the two are declared in; CSS would let the later declaration win, and it is the one place the two disagree, so do not set both on one edge.

The painted halves of a border have logical spellings too: border-inline-color, border-block-color, border-inline-style, border-block-style, their four -start-/-end- longhands each, and the four logical corners — border-start-start-radius, border-start-end-radius, border-end-start-radius and border-end-end-radius, named block edge first. This is what Tailwind’s border-s-*, border-x-<color> and rounded-s-* need.

inline-size, block-size and their min-/max- forms are plain aliases for width and height. They are not edges: which axis is inline is writing-mode’s to decide rather than direction’s, so with no writing modes here the inline axis stays horizontal however the direction runs.

Under UIToolkit the inline axis is always the physical one: UIElements has no start and end edge to resolve against, and direction does nothing there. The painted border properties are the exception — those are resolved before they reach UIElements, so they follow direction under both renderers.

Text decoration

text-decoration is a shorthand for the line and its colour. TextMeshPro draws the line from the font asset’s own metrics and has only a solid form, so the style and thickness components are read and then dropped — the point being that they do not take the line down with them:

a { text-decoration: underline red; }
a:hover { text-decoration: underline dotted 2px currentcolor; }
del { text-decoration: line-through #888; }

underline, line-through and none are the lines, and they combine. overline and blink are not accepted at all, because nothing can draw them, so a declaration asking for one is dropped. text-decoration-style and text-decoration-thickness do not exist as longhands for the same reason — there is nothing for them to set.

text-decoration-color defaults to currentcolor and, unlike on the web, is inherited: the line itself rides on font-style, which inherits, so a non-inherited colour would draw nested text’s underline in the wrong one. A colour that differs from color is applied through TextMeshPro’s rich text tags, which means it needs richText on — the default for a <text> — and is skipped when it is off.

font-style is one property carrying italic, bold and the letter casing as well as the two lines, so the shorthand sets only the components you name: text-decoration: red leaves the line as it is, where CSS would reset it to none.

The root element

:root is the element ReactUnity renders into, the one everything else is under. :host names the same element — there are no shadow trees here for the two to tell apart — so a stylesheet written for the web works either way, and :root, :host { ... }, which is how Tailwind opens its theme block, applies once.

:root, :host { --brand: #4f46e5; }
:host(.dark) { background-color: #0f172a; }

:host(...) matches the root only when the root also matches its argument, and the argument weighs what it would on its own. The root is an ordinary element in this: give it a class from C# with context.Host.ClassList.Add("dark") and the rules naming it restyle. :host-context() has nothing to ask — it is about the host’s ancestors, and the root has none — so it is read as a custom state like any other unknown pseudo-class.

Custom states

Any pseudo-class ReactUnity does not know is a custom state: view:busy matches while a state named busy is started on the element, from C# through component.StateStyles.StartState("busy") and EndState. :state(busy) is the standard spelling of the same thing, and the one to prefer, since a bare unknown name is warned about once in case it was a typo. Both take the specificity of a pseudo-class.

.slot:state(busy) {
  opacity: 0.5;
}