Osy#betaRun it on Osyrin: osyrin.com

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 file-manager sample, whose model/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 too — chevron_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, a palette step, or a color string, exactly what Text(color:) takes:

Icon(Icons.Chev, color: Colors.TextMuted)        // a muted separator in primary-colored text
Icon(Icons.Close, size: 24, color: Colors.Accent)
Icon(Icons.Check, color: Colors.Primary.Text)    // a palette step, as the text beside it

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.

An icon that points somewhere turns in Arabic#

An arrow, a chevron, a send or sign-out glyph points ONWARDS — to the right in English, to the left in a right-to-left language. Mark such a file on its own root, once: <svg data-mirror="rtl" viewBox="0 0 24 24">…</svg>. Every Icon(...) of it is then drawn mirrored in an Arabic page, and the right way round again inside an element with flowDirection: FlowDirection.LeftToRight — nothing at any call site. A logo, a clock, a play button or anything with writing in it has no mark and never turns. The platform's own arrow_left/arrow_right, chevron_left/chevron_right, send, login/logout, cart, external_link and message carry it. Any other value of data-mirror is a compile error naming the file. See Right to left — an app that reads in Arabic with no change to the app.

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.

⭐ A KIT CONTROL HOLDS THE SAME TYPE. Icons is built from YOUR files, so it looks like a type only your own app could name — but the compiler puts it in scope for the kits your app takes as well, and the vocabulary it hands them is yours, merged with the built-ins. That is why NavItem.Icon in the bundled app shell is an Icons: your icons/logo.svg is a nav row's glyph, a menu row's glyph and the shell's brand mark, written exactly as you write Icons.Search anywhere else. There is one icon vocabulary in an Osy# app and a kit does not get its own.

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 primitives — Row/Stack and the gap that spaces an icon from its label.
  • The file-manager sample (osy docs sample file-manager) — its model/icons/ folder is the vocabulary the first examples above resolve against. The default glob, **/icons/*.svg, finds an icons/ folder anywhere in the project — icons/ beside app.osy (what osy init scaffolds) and model/icons/ both work.

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`…

Right to left — an app that reads in Arabic with no change to the app

A language written right to left (Arabic, Hebrew, Persian, Urdu) mirrors the whole page: every horizontal side a style…