Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

icons

Icon(Icons.Search) · Icon(Icons.Search, size: 18) · Icons — an icon, named by a compile-checked identifier, and a type you can pass

Drop `.svg` files into `model/icons/` and render them with `Icon(Icons.Search)`. The name is checked at compile time, so a typo is an error rather than a blank square. An icon inherits the surrounding text size and color, so one icon set follows your theme through light and dark; pass `size:` when a glyph needs its own size. `Icons` is also a type, so an icon can be a parameter, a return value or a stored field.

stable3 examples compiled by CIuiiconsassets

Summary#

Icons are files in your app, not data. Put an .svg in model/icons/ and it becomes part of your app's vocabulary:

model/
  icons/
    search.svg
    close.svg
    menu.svg
[Composable]
component SearchBar() {
  string query = "";
  render {
    Row(gap: 2) {
      Icon(Icons.Search);
      Input(value: query, placeholder: "Search");
    }
  }
}

This compiles against the drop-ship-order sample, whose icons/ folder really does contain search.svg — which is the whole point: the name is checked against the files the app ships, so this example is wrong the moment that file is renamed.

Adding an icon is dropping a file in. There is nothing to register and nothing to import.

Signature#

Icon(Icons.Search) · Icon(Icons.Search, size: 18) — an icon, named by a compile-checked identifier

Description#

The name is checked#

Icon(Icons.Search) names the icon by a bare identifier, and it is checked against the icons your app actually declares. A typo is a compile error with a suggestion:

unknown icon 'serch' (declared icons: close, menu, search). Did you mean 'search'?

Because the name is an identifier, an icon's file name must be one toochevron_right.svg, not chevron-right.svg. A kebab-case file is rejected with the rename to make.

The name is never an expression. A local variable called search does not change what Icon(Icons.Search) means — the icon vocabulary always wins. This is deliberate: an icon chosen at runtime could not be checked, so there is no way to write one by accident.

Choosing an icon from data — a category's icon, say — is a content concern, not chrome. Use Image(src), or branch explicitly:

if (item.Kind == Kind.Folder) { Icon(Icons.Folder); } else { Icon(Icons.File); }

How big is an icon, and what color? — it inherits#

An icon is an em square that inherits the current text color. Put one beside a label and it matches at any font size, in light mode and dark, with nothing to configure:

Row(gap: 2) { Icon(Icons.Search); Text("Search"); }

Color defaults to the surrounding text color — any color you draw into the .svg is replaced by the current text color when your app is built, so an icon copied from any icon set immediately follows your theme. Override one glyph's color with color: — a theme token or a color string:

Icon(Icons.Chev, color: Colors.TextMuted)        // a muted separator in primary-colored text
Icon(Icons.Close, size: 24, color: Colors.Accent)

Like size:, color is a property of the glyph written on the call — never a tint wrapper around it. An outline icon (drawn as fill="none" plus a stroke) stays an outline.

Size defaults to the surrounding text size, and you override it per call with size: — a length:

Icon(Icons.Search, size: 18)       // 18px
Icon(Icons.Check, size: 14)        // 14px — a denser glyph
Icon(Icons.Menu, size: "1.5em")    // relative to the surrounding text

Size is a property of the glyph, so it is written on the call — never a wrapper component around it. If your app uses a handful of standard sizes, name them with an enum and pass the member (its value is the length):

enum IconSize { Sm = 14, Md = 18, Lg = 24 }

[Composable]
component SizedSearch() {
  render { Icon(Icons.Search, size: IconSize.Md); }
}

size: is the only argument Icon takes besides the name; a bare second argument or any other named argument is a compile error, so Icon(Icons.Search, 18) is corrected to Icon(Icons.Search, size: 18).

What an icon may contain#

An icon is shapes: path circle ellipse line polyline polygon rect g, and a viewBox on the root <svg>. Titles, descriptions and id/class attributes are dropped — they aren't needed.

Anything that could run, load, or reference something is a compile error, naming the file:

'evil.svg' contains a <script> element. An icon may only contain shape elements
(circle, ellipse, g, line, path, polygon, polyline, rect); scripting, styling,
embedding and animation are not allowed.

That covers <script>, <style>, <image>, <a>, <use>, animation elements, any on… handler, and any attribute that points somewhere (fill="url(#x)", xlink:href, a javascript: link). An SVG is a place scripts can hide, and your icons are placed directly into your app's pages — so the rule is an allow-list, and it is not negotiable.

An icon with no drawable content left, or with no viewBox, is also an error rather than an invisible square.

Icons from a UI kit#

A kit's icons land in your app tree alongside your own, and are picked up the same way. Two files claiming the same name is an error naming both, so an icon always resolves to exactly one file.

Passing an icon around — Icons is a type#

Icons is not only a spelling for a call site: it is a type, so an icon is a value you can pass, return, store and compare like any other.

[Composable]
component NavItem(string label, Icons icon) {
  render {
    Row(gap: 2) {
      Icon(icon);
      Text(label);
    }
  }
}

The caller names the glyph the same way it always did:

NavItem("Search", Icons.Search);

The same type works in every other position — a return type, a local, a parameter to an ordinary function, and a field on an entity:

entity NavEntry {
  string Label;
  Icons Glyph;                       // stored, keyed by the file's own name
}

Icons GlyphFor(bool searching) {
  return searching ? Icons.Search : Icons.Close;
}

Because a stored icon is keyed by the file's name and not by a position in a list, adding a new .svg to your app never changes what an already-stored row means.

How icons are delivered — inline, on the first byte#

Your icons are combined into one small file, and it is already in the page when it arrives — a server-rendered page paints its icons on the very first byte, before any script runs. There is no icon-flash, no request, and nothing to configure. Change an icon and the file changes with it; leave them alone and browsers keep the copy they already have.

Custom glob, if model/icons/ doesn't suit you:

app Admin {
  model "model/**/*.osy";
  icons "assets/icons/*.svg";
}

See also#

  • theme tokens — the tokens an icon's color resolves against.
  • layout primitivesRow/Stack and the gap that spaces an icon from its label.
  • The drop-ship-order sample (osy docs sample drop-ship-order) — its icons/ folder is the vocabulary the compiled examples above resolve against, and its storefront header uses one.

Related

theme tokens

A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can…

layout primitives

The built-in layout primitives and how they arrange children. `Stack` stacks children in a column, `Row` lays them in a…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…