Skip to main content

Style Syntax

A style is defined in a .style file and contains definitions for how an element should look, how it should be sized and positioned, and how it behaves with regards to clipping, input, and text handling.

Basics

Every style begins with a style keyword and then a unique name. The definition of the style itself come with a set of curly braces. Inside this definition are the properties and values that should apply to that element.

Below is a simple style for a 100 by 50 pixel box that colored orange.

// comments use the double slash syntax
style orange-box {
PreferredWidth = 100px;
PreferredHeight = 50px;
BackgroundColor = orange;
}

Constants

It can be convenient to declare some values as reusable constants. You can do this with the const keyword. Constants are defined only at the outer most scope in the file. Lets say that orange-box style should instead have a color value defined from a constant.

const boxColor = hotpink;

style colored-box {
PreferredWidth = 100px;
PreferredHeight = 50px;
BackgroundColor = @boxColor; // constants are referred to with the '@' operator
}

Queries

Queries are simple ways to style elements based on some application state. To build on our colored-box example from before, let's apply an outline color to the box when it gets hovered by the mouse.

const boxColor = hotpink;

style colored-box {
PreferredWidth = 100px;
PreferredHeight = 50px;
BackgroundColor = @boxColor;

// apply a 2 pixel wide black outline on hover
[hover] {
OutlineWidth = 2px;
OutlineColor = black;
}

}

An in depth guide to which queries are available is here

Queries can also be nested. We can make our colored-box outline appear only when we have an attribute outlined and our box is currently hovered by nesting two queries. Queries can be nested as deep as you like but you can have at most 63 queries on a single style regardless of nesting depth.

style colored-box {
PreferredWidth = 100px;
PreferredHeight = 50px;
BackgroundColor = @boxColor;

[attr:outlined] {
[hover] {
// only apply the outline when we have the 'outlined' attribute and we are hovered
OutlineWidth = 2px;
OutlineColor = black;
}
}

}

An important concept with styling is the idea of precidence. This determines how competing styles figure out who wins.

Let's add another query to highlight this.

style colored-box {
PreferredWidth = 100px;
PreferredHeight = 50px;
BackgroundColor = @boxColor;

// apply a 2 pixel wide black outline on hover
[hover] {
OutlineWidth = 2px;
OutlineColor = black;
}

// make the outline color white when we have the 'outlined' attribute applied
[attr:outlined] {
OutlineColor = white;
}

}

So now we have a problem. Both the [hover] query and the [attr:outlined] query define a color. When we hover the element, which color will it end up being? Each 'block' of styling applied to an element has a precedence score. The more nested a block is, the more precedence it has. The stateful queries [hover], [focus], and [active] have a higher level importance even though they might be defined at the 'depth' as another query. So in the end because of its elevated importance, the [hover] query wins out over the [attr:outlined] and the outline color will be black.

Mixins

Mixins are reusable styles that can optionally be parameterized. Let's add some behavior to colored-box from a mixin.

Let's say we want a reusable style that applies rounding to the corners of an element when it's hovered. Here is how we'd do that with a mixin.

mixin rounded-hover {
[hover] {
CornerRadius = 50%;
}
}

style colored-box {
PreferredWidth = 100px;
PreferredHeight = 50px;
BackgroundColor = @boxColor;
mixin(rouned-hover); // this basically copy-pastes the contents of the 'rounded-hover' mixin into this statement.
// any queries or other properties are also copied
}

Let's take that example one step further and make it a function that can use a variable for how much rounding we apply.

// we introduce a parameter called roundness with a default value of 50%;
mixin rounded-hover-with-parameter(roundness = 50%) {
[hover] {
CornerRadius = %{roundness}; // use our parameter instead of applying a hard coded value
}
}

style colored-box {
PreferredWidth = 100px;
PreferredHeight = 50px;
BackgroundColor = @boxColor;

// invoke our mixin but assign roundness to 10px instead of the default 50%
mixin(rouned-hover-with-parameter) {
roundness = 10px;
}
}

Styles can also be mixed into other styles and behave just like mixin except they cannot provide parameters.

style mix-me {
Margin = 20px;
Padding = 20px;
}

style colored-box {
PreferredWidth = 100px;
PreferredHeight = 50px;
BackgroundColor = @boxColor;

// re-use the 'mix-me' style's properties to apply both margin and padding to this box

mixin(mix-me);

}

Variables

We talked about constants before which lets us define values one place and reuse them across multiple styles without having to go find those values later if we want to change them. This can go one step further with variables. Variables let us define values dynamically and then makes those values available to all descendant elements too. Here is an example where we want to make a Button element that dynamically define the color of its text based on a variable.

style <Button> {

[attr:primary] {
set buttonText = red;
}

[attr:secondary] {
set buttonText = green;
}

}

style button-text {
// variable syntax is using the `var` function that has the first argument set to the variable we want to read
// and the second argument is the value to use if the variable is not defined.
TextColor = var(buttonTextColor, white);
}

// in a .ui file

template Button {
Text("Click Me", style = [@button-text]);
}

template ButtonSample {
Button(); // text is white since we have no variable set we use the default
Button(attr:primary); // text is red
Button(attr:secondary); // text is green
}

Transitions

Transitions are easy-mode animations. They basically work by observing style properties and when the property value changes, instead of setting the new value immediately, it will smoothly transition from the old value to the new value.

Here is how it works

style AnimateColor {
// define a transition that uses an easing curve of QuarticEaseIn,
// has a duration of 1000ms and applies to the BackgroundColor property
transition QuarticEaseIn 1000ms = BackgroundColor;

BackgroundColor = white;

// transition will work from any change source, but hover is an easy one to demonstrate
[hover] {
BackgroundColor = black;
}

}

Transitions can be defined in any block scope and apply as long as that scope is active and has the highest precedence.

The basic syntax is

// basic syntax is the transition keyword, 
// then the interpolator
// then the duration (if applicable)
// then the delay (optionally)
// then = a comma seperated list of properties to apply to
transition *easingfn* 1000ms 500ms? = PreferredWidth, BackgroundColor;

There are three interopolator options for transitions. The most basic is the easing mode. All of the easing functions listed here are supported.

Easing Transitions

When defining a transition with an ease interpolator, a duration is required and a delay is optional.

style ease-circ {
// circular ease in with a 2 second duration and a half second delay
transition EaseInCirc 2000ms 500ms = PaddingLeft;
}

Bezier Transitions

You can also provide your own bezier curve definition. You probably want to use a tool like this to find the right numbers for the desired curve.

Just like easing curves, bezier transitions also require a duration and optionally accept a delay

transition bezier(0.17, 0.67, 0.83, 0.67) 500ms = PreferredWidth;
transition bezier(0.17, 0.67, 0.83, 0.67) 500ms 200ms = PreferredWidth;

Spring Physics Transitions

Springs are pretty awesome. Instead of using a curve and fixed duration, they run until they settle Settling means that the motion provided by the spring is very close to 0. This provides much more natural feeling animations that are very pleasing to the human eye because they based on nature and not arbitrary timing values. Here is a great resource on how they can be use for animation

Here are some other ways to define spring transitions. There are a few default spring configurations that exist as keywords

  • default
  • gentle
  • stiff
  • wobbly
  • slow
  • superslow

You can define a spring using one of those keywords, a constant, or by providing the values directly.

const mySpring = wobbly;
transition spring(@mySpring) = PreferredWidth;
transition spring(gentle) = PreferredWidth;
transition spring(default) = PreferredWidth;
transition spring(120, 10) = PreferredWidth; // stiffness, damping
transition spring(120, 10, 1) = PreferredWidth; // stiffness, damping, mass

// stiffness, damping, mass, precision (how close to 0 we need to be in order to be considered 'settled'
transition spring(120, 10, 2, 0.1) = PreferredWidth;