Icons (Material + Lucide)¶
Your app needs a menu glyph, a lock in the password field, a back arrow? tempestweb ships two vendored icon sets — Lucide (the default, inherited from the core) and Material Symbols (Outlined) — drawn client-side as inline SVG. No icon font, no network, no CDN: it works offline and inside a PWA. 🎯
You pick the set in one obvious call, in typed Python:
from tempestweb.icons import material_icon, lucide_icon, MaterialIcons, Icons
material_icon(MaterialIcons.HOME) # Material Symbols "home"
lucide_icon(Icons.MAIL) # Lucide "mail"
The minimum: one icon on screen¶
An Icon is a widget like any other. Drop it in a Column/Row and you're done:
from dataclasses import dataclass
from tempest_core import App, Column, Row, Text, Widget
from tempestweb.icons import MaterialIcons, material_icon
@dataclass
class State:
pass
def make_state() -> State:
return State()
def view(app: App[State]) -> Widget:
return Row(
children=[
material_icon(MaterialIcons.HOME),
Text(content="Home"),
],
)
Run it in both modes — the drawing is identical:
tempestweb run --mode wasm # Python in the browser (Pyodide)
tempestweb run --mode server # Python on the server (FastAPI + WebSocket)
Why two sets?
Lucide is the core's default set (tempest_core.icons.Icons) — clean,
"feather"-style strokes. Material Symbols pairs with the always-on
Material 3 base theme (see Theming). Use whichever matches
your app; you can mix both on the same screen.
Picking an icon with autocomplete¶
MaterialIcons and Icons are StrEnums — each member is its string. So you
get editor autocomplete without losing the freedom to pass a raw string:
from tempestweb.icons import MaterialIcons, material_icon
material_icon(MaterialIcons.SETTINGS) # with autocomplete
material_icon("settings") # raw string — identical
Any name in the set works
The enum lists the most common icons (HOME, SEARCH, CLOSE, MENU…), but any
valid Material Symbols name works as a raw string as long as the glyph is
vendored on the client (client/icons/material.js). For a glyph outside the
set, see Custom icon below.
Size and color¶
size is the icon's edge in logical pixels. Omit it to let the icon scale with
the surrounding font. Color comes from Style.color — the glyph is drawn in
currentColor:
from tempest_core import Style
from tempest_core.style import Color
from tempestweb.icons import MaterialIcons, material_icon
# A 20px icon tinted red
material_icon(
MaterialIcons.FAVORITE,
size=20.0,
style=Style(color=Color(r=220, g=40, b=40)),
)
# No size → follows the container's font size
material_icon(MaterialIcons.STAR)
Icons inside fields¶
The ready-made fields (see Ready-made components) accept icons in
their slots by raw name — and an unprefixed name stays Lucide, for
compatibility with the core's Icon:
from tempestweb.components import EmailField, PasswordField
EmailField(value="", on_change=..., leading_icon="mail") # Lucide
PasswordField(value="", on_change=..., leading_icon="lock") # Lucide
The name grammar
Under the hood, the set is encoded as a prefix on the Icon name:
"material:home", "lucide:mail". The material_icon/lucide_icon helpers
add the prefix for you. An unprefixed name ("mail") resolves to Lucide —
which is why the field slots only ask for the bare name.
Custom icon (raw SVG)¶
Need a glyph that is not vendored? Two ways out.
1. custom_icon — ship the path over the wire, register nothing. The SVG d
rides in the icon name itself, so the client needs no prior registration. It's
drawn stroked on a 0 0 24 24 grid in currentColor (the Lucide convention):
from tempestweb.icons import custom_icon
# A hand-drawn "bolt" on the 24x24 grid
custom_icon("M13 2 L4 14 H12 L11 22 L20 10 H13 Z", size=24.0)
2. Register on both sides — for a reused glyph. Register in Python
(register_icon) and on the client (registerIcon in
client/icons/index.js), then pass the bare name:
from tempest_core import Icon
from tempestweb.icons import register_icon
register_icon("rocket", "M13 2 L4 14 H12 L11 22 L20 10 H13 Z")
Icon(name="rocket")
import { registerIcon } from "./icons/index.js";
registerIcon("rocket", "M13 2 L4 14 H12 L11 22 L20 10 H13 Z");
When to use which
custom_icon is great for a single, one-off glyph (no client change). The
both-sides registration is worth it when the same glyph shows up in many
places — the path travels once (in the vendored JS) instead of in every patch.
Offline and PWA¶
Because everything is inline SVG from path data vendored in
client/icons/{lucide,material}.js, the icons make no network requests.
tempestweb build bundles those assets into the artifact, so an installed app
(PWA) draws every icon offline, with no icon font and no external request.
Nothing to configure. ✅
Recap¶
- Two vendored sets:
lucide_icon(...)(default) andmaterial_icon(...)(pairs with the Material 3 theme). MaterialIcons/IconsareStrEnums → autocomplete + raw string.sizescales the icon (omit to follow the font);Style.colortints it.- Field slots take a bare name (= Lucide).
- Glyph outside the set:
custom_icon(path)(one-off) orregister_icon+registerIcon(reused). - It's all inline SVG — no network, offline/PWA safe.