Skip to content

Gradients & Effects

A node's style: can paint a gradient, blur itself or what is behind it, blend with what is under it, filter its colours, stick to the top of a scroll view and choose its pointer. All of them work in a stylesheet too. Every key is listed in the YAML reference.

Gradients

background, foreground and border_color take a gradient where they take a colour. Write one as a CSS-like string:

id: root
kind: Container
style: {flex_direction: vertical, gap: 12, padding: 16, width: 360, background: surface}
children:
  - id: banner
    kind: Rect
    style: {height: 64, corner_radius: 16, background: "linear-gradient(90deg, primary, tertiary)"}
  - id: glow
    kind: Rect
    style: {height: 64, corner_radius: 16, background: "radial-gradient(at 30% 30%, primary_container, surface)"}
  - id: title
    kind: Text
    text: {content: "Gradient text", typography_role: headline_medium}
    style: {foreground: "linear-gradient(to right, primary, error)"}
  • linear-gradient([angle | to side,] stop, stop, ...): the angle is in deg (0 up, 90 right, 180 down, the default), or to right, to bottom left and the like.
  • radial-gradient([at x y,] stop, ...): outward from a point, the middle unless at 30% 30% says otherwise.
  • conic-gradient([from angle] [at x y,] stop, ...): around a point.
  • A stop is a theme role or a CSS colour, with an optional place: primary 40%. Stops with no place are spread evenly between the ones that have one.

The mapping form says the same with keys, and is what to write when a string is awkward:

style:
  background: {gradient: linear, angle: 45, stops: [primary, [0.6, secondary], tertiary]}
  border_width: 3
  border_color: {gradient: sweep, stops: [primary, secondary, primary]}

gradient is linear, radial or sweep (conic); angle, center ([x, y] as fractions of the box), radius and start go with the kind. A gradient follows its box as the layout resizes it. A control's own colour (a Switch, a Slider) takes a plain colour only.

Elevation that moves

frosted: true does this for the navigation surfaces: NavigationRail, NavigationDrawer, NavigationDrawerModal, Menu, SearchBar, SearchView and TopAppBar become 72% of their colour with a 20 pixel backdrop blur. It is off by default.

An Icon whose glyph changes can morph into the next with transition: {icon: 200}: two closed shapes, or two open lines, in the same set of glyphs (Material Symbols or MDI) blend smoothly; shapes with different numbers of parts switch halfway, and glyphs of the other set switch at once. IconButton does this for its toggle icons.

A raised surface lifts when the pointer is over it and again when it is pressed. Give the levels in rules by state, and transition: {elevation: 200} eases the shadows between them. The base level belongs in a rule too: an elevation written on the node itself beats every rule.

styles:
  - widget: Card
    style: {elevation: 1, transition: {elevation: 200}}
  - widget: Card
    state: hovered
    style: {elevation: 3}
  - widget: Card
    state: pressed
    style: {elevation: 5}

A press during a hover carries on from where the shadow is. A level a view works out itself, such as a lift while dragging, eases the same way: style: {elevation: "{{ 4 if dragging else 1 }}", transition: {elevation: 200}}. An app that reduces motion gets the level at once. Levels are 0 to 5, or level_0 to level_5.

Corner radius

corner_radius is one radius for every corner (pixels, or a shape token none ... extra_large, full). Give four to round the corners differently, as a list [top_left, top_right, bottom_right, bottom_left] or as a mapping of corners and edges, where an edge (top, right, bottom, left) is its two corners, a corner named beats its edge and the corners left out are square:

widget: Container
style: {width: 120, height: 48, background: primary, corner_radius: {left: full, top_right: 4}}   # a pill end on the left

It is a style field like the others, so a rule can give it, a transition: eases it, and the state layer and focus ring follow the shape.

Blur, and a frosted surface

blur blurs the node and what it draws, by that many pixels. backdrop_blur blurs what is behind it, so a node with a translucent background and a backdrop_blur is a frosted pane:

- id: pane
  kind: Container
  style: {width: 240, height: 120, background: "#FFFFFF55", backdrop_blur: 16, corner_radius: 20}

Both can animate. A pane only has something to blur when other nodes are drawn behind it.

Blend modes and colour filters

blend_mode draws a node over what is behind it the way CSS's mix-blend-mode does: normal (the default), multiply, screen, overlay, darken, lighten, color_dodge, color_burn, hard_light, soft_light, difference, exclusion, hue, saturation, color and luminosity.

filter changes the colours of a node and everything in it, as CSS filters do; the filters apply in the order written:

Key Unchanged at Range
saturate, brightness, contrast 1 0 and up
grayscale, invert, sepia 0 0 to 1
hue_rotate 0 degrees

A disabled-looking card is filter: {grayscale: 1, brightness: 1.2}.

- id: card
  kind: Container
  style: {width: 200, height: 80, background: primary_container, filter: {saturate: 0.2}}

Sticky headers

In a ScrollView, sticky: N makes a node hold the top edge of the scroll view, N pixels down, as CSS's position: sticky does: it scrolls with its content until it reaches that edge, then stays there while the container it is in scrolls past, and the end of that container pushes it out. Put a header in each section:

- id: list
  kind: ScrollView
  style: {height: 240, flex_direction: vertical}
  children:
    - id: fruit
      kind: Container
      style: {flex_direction: vertical}
      children:
        - id: fruit_header
          kind: Rect
          style: {height: 40, background: surface_container_high, sticky: 0, z_index: 1}
        - id: apple
          kind: Rect
          style: {height: 56, background: surface}

The node is only moved when it is drawn: layout, hit testing and its siblings don't change. Content later in the section paints over a stuck header unless the header has a z_index.

Transitions and transforms

transition: in a style says which changes ease to their new value instead of jumping. A node is where its style says when it is first drawn; the next time the style gives a property a new value, it travels there:

- id: card
  kind: Rect
  style:
    width: 200
    height: 80
    background: surface_container
    corner_radius: 12
    transition:
      background: 150                                   # milliseconds, with the standard easing
      corner_radius: {duration: 200, easing: emphasized_decelerate}
      scale: {duration: 300, easing: spring, bounce: 0.3}
      opacity: {duration: 100, easing: [0.2, 0, 0, 1]}  # or four numbers: a cubic bezier
  • The keys are style fields: background, foreground (a Text's or Icon's colour), border_color, border_width, corner_radius, elevation, opacity, blur, backdrop_blur, scale, translate_x, translate_y and rotation_deg. all: 150 covers every one of them; a named key wins over all.
  • A value is a number of milliseconds, or {duration, easing, bounce}. The easings are linear, Material's standard, standard_accelerate, standard_decelerate, emphasized, emphasized_accelerate and emphasized_decelerate, four numbers (a cubic bezier, as CSS writes cubic-bezier), and spring, whose duration is its period and whose bounce runs from -1 to 1.
  • Layout eases too: width, height, x, y, gap, padding and margin (all leaves these out, since a layout that changes is not always meant to glide). The engine cannot animate layout yet, so Tesserae sets the value each frame along the same curve (a spring is drawn as a settling curve); a value that is not a number, such as auto or 50%, changes at once, and a change begun in the middle of another goes on from where the first had got to.
  • When the app has asked for reduced motion, a change happens at once.
  • A transition is part of a style, so a stylesheet rule can carry it: a rule for state: hovered that changes background eases if the rule for the widget gives a transition.

scale (1 is unchanged), translate_x, translate_y (pixels) and rotation_deg (clockwise, about the node's centre) draw a node transformed, and can be eased too. They change how it is drawn, not how much room it takes.

The pointer

cursor is the name of a pointer shape (pointer, text, grab, grabbing, move, not_allowed, wait, progress, crosshair, help, copy, the resize pointers and the rest, listed in the YAML reference), or a picture next to the view:

style: {cursor: {src: pointers/pen.png, hotspot: [2, 30]}}

The picture is read with the view, like an Image's: relative to the view's folder, inside it, 1 to 256 pixels a side, and hotspot is the pixel that is the pointer's place. It is in device pixels, so give a larger one for a high-density screen. A picture is only given in a node's own style:, not in a stylesheet. A Link keeps pointer unless it says another.