Skip to main content

Photo & GIF Frame

Render an uploaded photo or animated GIF beside a label. The embedded fallback is a real two-frame GIF, so this example works before a user selects media.

Animated pixel-art heart frame labeled Memory

Complete app

app.star
load("encoding/base64.star", "base64")
load("render.star", "render")
load("schema.star", "schema")

DEMO_GIF = """R0lGODlhIAAYAPQAAAAAAOxImexJmexJmuxLmuxMm+1OnO1Qnu1Tn+1Woe9mqvB0
svF9t/KBufOOwfSax/WdyfWjzfat0vi92/i/3PjC3vnG4PnH4fnN5PrR5/rS5/vZ
6/zn8wAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBFAAAACwAAAAAIAAY
AAAFjSAgjmRpnmiqrkHrvnAsk3Jtv/Stx/m7VJzKIvYLDmG9VoTDZEZeyybniRv5
pM3jAss8tpJAbqUVxo5dSa5aXRW9LOs4x9IGXOVqbyAZiOKbVGhWMAsYfxt6X4M
xDXIaNXwvDnEJkIsyEGoKNpEwElgMN50wFE0QOqMwGhwUO6kvCBwIrpc7tCu4ub
q6IQAh+QQBFAAAACwAAAAAIAAYAIQAAACLXPaMXfaNXvaOYPaPYfaQY/aSZveUa
PeXbfeed/isi/mwkfm2mvq+pfq/p/rJtvvTxPzUxvzazvzbzvzbz/3g1/3t6f4A
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFkyAgjmRpnmiqrkHrvnAsk3Jtv/Stx7lL
MJPLhEF4/YLDIm70Slye0Eui5Yw+p64ewRolbLlPZaDHAD8ZZTMjy2wFzZM3eMI
Wucx4ax3gkufney5pf2BrLVqEZmI9AVWJUFiHbT5AhEkwjDANeJGYkzIPYAo2mT
EQVgs3pTESUA86qzAEFhcSO7EwBxcHtyu+v8DBIQA7"""

def main(config):
encoded = config.get("photo") or DEMO_GIF
image = render.Image(src = base64.decode(encoded))

return render.Root(
child = render.Padding(
pad = (2, 4, 0, 4),
child = render.Row(
expanded = True,
main_align = "space_between",
cross_align = "center",
children = [
render.Text(
"MEMORY",
font = "CG-pixel-3x5-mono",
color = "#ffffff",
),
image,
],
),
),
)

def get_schema():
return schema.Schema(
version = "1",
fields = [
schema.PhotoSelect(
id = "photo",
name = "Photo or GIF",
desc = "Image displayed inside the frame.",
icon = "image",
),
],
)

Download this app.star.

How it works

  1. PhotoSelect stores the chosen image as Base64 configuration data.
  2. The fallback uses the same shape, so base64.decode() always gives Image real image bytes.
  3. Image preserves the embedded GIF's frames and timing.
  4. An expanded Row keeps the label and image separated, while center alignment places the label halfway down the 24-pixel image area.

Keep production GIFs at 64×32, no larger than 128 KB, and verify the complete loop as described in the GIF guide.

Was this helpful?