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 identifierDescription#
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 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 textSize 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 primitives —
Row/Stackand thegapthat spaces an icon from its label. - The drop-ship-order sample (
osy docs sample drop-ship-order) — itsicons/folder is the vocabulary the compiled examples above resolve against, and its storefront header uses one.