Things to remember
Always add key= to inputs inside loops or conditionals — otherwise guile can't tell them apart after a re-render.
State lives outside ui() — declare it at module level, or it resets on every render.
Don't call .set() unconditionally inside ui() — it triggers another render and loops forever. Use on_change= callbacks instead.
Lambda captures loop variables late — lock in the current value with a default arg: lambda i=i: delete(i)
State
count = gui.state(0) # int
name = gui.state("") # str
items = gui.state([]) # list
active = gui.state(True) # bool
count.set(42) # replace
count.update(lambda x: x+1) # transform
count.toggle() # bool flip
gui.text(count) # State directly — no .value
gui.text(f"{count.value} items")
Layout — indentation = nesting
with gui.col(padding=20, gap=12):
with gui.card():
gui.title("Hello")
with gui.row(gap=8, justify="flex-end"):
gui.button("Cancel", variant="ghost")
gui.button("Save")
# gap space between children (px)
# padding space inside the container
# align cross-axis: "center", "stretch"
# justify main axis: "flex-end", "space-between"
Inputs
gui.button("Save", on_click=save)
gui.button("Del", variant="danger", size="sm")
name = gui.input("Name", key="name")
gui.text(f"Hello {name.value}")
temp = gui.slider("°C", min=0, max=100, key="temp")
ok = gui.checkbox("Agree", key="ok")
unit = gui.select(["kg", "lb", "g"], "Unit", key="unit")
# bind to existing state
gui.slider("°F", value=fahrenheit,
on_change=fahrenheit.set, key="f")
Patterns
# Conditional — plain Python
if count.value > 0:
gui.badge("positive", variant="success")
else:
gui.badge("zero", variant="neutral")
# Loop — key= keeps updates fast
for i, item in enumerate(items):
with gui.row(key=f"row-{i}"):
gui.text(item)
gui.button("✕", on_click=lambda i=i: delete(i),
key=f"del-{i}")
# Toast — call from callback, never inside ui()
gui.notify("Saved!", variant="success")
Data & Plots
import pandas as pd
import matplotlib.pyplot as plt
# DataFrame, list of dicts, numpy array
gui.table(df)
gui.table(df, columns=["date", "value"])
fig, ax = plt.subplots()
ax.plot(x, y)
gui.figure(fig)
gui.figure(fig, caption="Revenue", dpi=120)
Themes & App options
gui.theme("dark") # call first inside ui()
gui.theme("light", primary="#e11d48")
# presets: light dark neon rose forest slate
@gui.app("My App",
width=800, height=600,
center=True, # center content
resizable=True, # allow resize
debug=True) # open devtools
def ui(): ...
Widget quick reference
| widget |
common options |
notes |
gui.col() gui.row() |
gap padding align justify fill scroll |
use as with block — stack children vertically / horizontally |
gui.card() |
gap padding margin fill |
raised surface; use as with block |
gui.title() gui.text() |
size bold muted style |
size: xs sm md lg xl 2xl 3xl — accepts State directly |
gui.badge() |
label variant |
variant: primary success danger warning neutral |
gui.button() |
on_click variant size disabled |
variant: primary secondary ghost danger |
gui.input() |
label placeholder value on_change key |
returns State[str] — use .value to read |
gui.number_input() |
value min max step unit on_change |
returns State[float] — no string conversion needed |
gui.slider() |
min=0 max=100 step=1 value on_change |
returns State[float] |
gui.checkbox() |
label value on_change key |
returns State[bool] |
gui.select() |
options label value on_change key |
list[str] or [(val, label), …] → State[str] |
gui.tabs() |
labels value on_change key |
returns active tab as str — always add key= |
gui.table() |
data columns max_rows=2000 |
DataFrame / list of dicts / 2-D list / numpy array |
gui.figure() |
fig dpi=96 width caption |
any matplotlib Figure |
gui.notify() |
message variant duration=3.0 |
toast — call from a callback, not inside ui() |