gif widget
The gif widget plays animated images at native panel resolution — each LED maps to one pixel in the source, not a scaled block. Format-agnostic: anything Pillow opens with n_frames and seek() works (.gif, animated .webp, .apng, multi-frame .tiff). Per-frame durations come from the file’s own metadata.
Set text and it appears on the panel alongside the animation. When the text is longer than the canvas, set text_align = "scroll" or "scroll_over" for a marquee. For two-row layouts (held top + scrolling bottom), set bottom_text to switch modes.

Minimal example
[[playlist.section.widget]]type = "gif"path = "config/assets/my_animation.gif"Options
Section titled “Options”Common patterns
Section titled “Common patterns”Silent gif (no text)
Section titled “Silent gif (no text)”
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"fit = "pillarbox"image_align = "center"play_count = 3Scrolling marquee over the image (scroll_over — text in front)
Section titled “Scrolling marquee over the image (scroll_over — text in front)”
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"fit = "pillarbox"image_align = "left"text = "Welcome to the show!"text_align = "scroll_over"font_color = [225, 48, 108]play_count = 0 # play through hold_timeScrolling text behind a transparent silhouette (scroll — text walks under the image)
Section titled “Scrolling text behind a transparent silhouette (scroll — text walks under the image)”
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"fit = "pillarbox"image_align = "left"text = "Scrolling behind the phoenix!"text_align = "scroll"font_color = [200, 200, 255]play_count = 5Wrap mode (seamless marquee with a separator between repeats)
Section titled “Wrap mode (seamless marquee with a separator between repeats)”
Setting text_wrap = true on a scroll or scroll_over marquee runs the text seamlessly — at every tick at least one full copy of the message is on the panel, with a configurable separator drawn between repeats. Use this for short slogans or breaking-news banners where the gap of an empty canvas between traversals breaks the rhythm.
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"text = "BREAKING NEWS"text_wrap = truetext_align = "scroll_over"text_separator = " * " # default: " • "text_separator_color = "rainbow" # default: inherit font_color| Field | Type | Default | Meaning |
|---|---|---|---|
text_wrap | bool | false | Toggle seamless wrap. Requires text_align ∈ ("scroll", "scroll_over"). |
text_separator | string | " • " (when text_wrap = true) | Glyph(s) between repeats. "" falls back to a two-space gap. |
text_separator_color | color spec | inherit font_color | Color for the separator; whole-string provider (one hue per frame). |
Notes:
text_loopsin wrap mode means minimum number of cycle traversals (one cycle = text + separator), not the off-right → off-left definition the default marquee uses. On thetwo_rowwidget the equivalent field isbottom_text_loops(prefixed to matchbottom_text_wrapand friends on that widget).- Two-row mode (
bottom_textset) refusestext_wrapin v1. - The separator renders in the widget’s
font; a per-separator font override isn’t part of v1.
Wrap mode (bottom row, seamless marquee)
Section titled “Wrap mode (bottom row, seamless marquee)”
Setting bottom_text_wrap = true in two-row mode (when bottom_text is set) runs the bottom row as a seamless marquee — chasing itself across the canvas with a separator between repeats. Top row stays held at top_align; only the bottom row wraps. Engages even when the bottom text fits — predictable.
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"top_text = "BREAKING"top_color = [225, 48, 108]bottom_text = "tap to subscribe"bottom_text_wrap = truebottom_text_separator = " * " # default: " • "bottom_text_separator_color = "rainbow" # default: inherit bottom_color| Field | Type | Default | Meaning |
|---|---|---|---|
bottom_text_wrap | bool | false | Toggle seamless wrap on the bottom row. Requires bottom_text non-empty. |
bottom_text_separator | string | " • " (when bottom_text_wrap = true) | Glyph(s) between bottom-row repeats. "" falls back to a two-space gap. |
bottom_text_separator_color | color spec | inherit bottom_color | Color for the bottom separator; whole-string provider (one hue per frame). |
Notes:
bottom_text_wrapalways wraps when set, even if the bottom text fits the canvas.- Top row never wraps (refused by validation; no
top_text_wrapfield exists). - The separator color inherits
bottom_color(NOTfont_color) — separator is part of the bottom row. - v1’s
text_wrapstays single-row only — in two-row mode usebottom_text_wrap. bottom_text_wrapis only allowed inmode = "slideshow". Inticker/one_at_a_timemodes, validation refuses it (those modes expect widgets to terminate naturally).
Scroll-through mode (bottom row, one offscreen pass)
Section titled “Scroll-through mode (bottom row, one offscreen pass)”
Setting bottom_text_scroll = "scroll_through" in two-row mode forces the bottom row to do an offscreen-to-offscreen scroll on every visit — text starts fully off the right edge and ends fully off the left edge — instead of the default “held when it fits, scroll only on overflow” behavior. bottom_align is ignored.
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"top_text = "NOW PLAYING"bottom_text = "Phoenix Rising - The OST Collection (2026)"bottom_text_scroll = "scroll_through"| Field | Type | Default | Meaning |
|---|---|---|---|
bottom_text_scroll | string | "marquee" | Bottom-row scroll style. "marquee" preserves the held-on-fit / scroll-on-overflow default; "scroll_through" forces offscreen-to-offscreen passes on every visit. |
Notes:
- Mutually exclusive with
bottom_text_wrap = true— pick one. Scroll-through is one pass; wrap is a seamless tile. - Requires non-empty
bottom_text(two-row mode). Single-row text scroll lives undertext_align = "scroll"/"scroll_over". - Only valid in
mode = "slideshow". Inticker/one_at_a_timemodes validation refuses it (rule 32). hold_timeis honored as a floor: at least one full pass per visit, ortext_loops × (canvas_width + bottom_width)if larger, or whateverhold_timeadmits — whichever is greatest. The bottom row keeps cycling until that floor is met.- Loop count for the bottom-row wrap: use
text_loopshere on gif/image widgets (same field that controls single-row wrap loops). On thetwo_rowwidget — which is always two-row, not optionally — the equivalent field is namedbottom_text_loopsto match its bottom-prefix convention. The validator catches the cross-widget typo (rule 29) and points you at the right name.
Two-row text overlay (set bottom_text to switch modes — image plays underneath)
Section titled “Two-row text overlay (set bottom_text to switch modes — image plays underneath)”
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"fit = "pillarbox"image_align = "right"top_text = "@firebird.demo"top_font = "5x8"top_color = [225, 48, 108]bottom_text = "Vinyasa · yin · hot power · restorative · slow flow · all levels"bottom_font = "5x8"bottom_color = [255, 240, 200]play_count = 3Typewriter overlay (single-row only — letters type in next to the animation)
Section titled “Typewriter overlay (single-row only — letters type in next to the animation)”
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"fit = "pillarbox"image_align = "left"text = "hi friend"text_align = "right"font_color = "rainbow"animation = "typewriter"play_count = 0 # play through hold_timeTypewriter + rainbow border (frame the panel as the text appears)
Section titled “Typewriter + rainbow border (frame the panel as the text appears)”
[[playlist.section.widget]]type = "gif"path = "config/assets/phoenix_transparent.gif"fit = "pillarbox"image_align = "left"text = "hi friend"text_align = "right"font_color = [255, 255, 255]animation = "typewriter"border = "rainbow"play_count = 0 # play through hold_timePlaying through hold_time: play_count = 0
Section titled “Playing through hold_time: play_count = 0”When you want a gif to fill its section’s duration rather than playing a fixed number of times, set play_count = 0. The engine computes how many complete loops fit in hold_time and plays at least one even if the time budget is shorter than the gif’s own duration.
[[playlist.section]]mode = "slideshow"hold_time = 8.0
[[playlist.section.widget]]type = "gif"path = "config/assets/sparkles.gif"play_count = 0 # fills the section's hold_timeplay_count >= 1 plays the exact count regardless of hold_time.
Typewriter is single-row only
Section titled “Typewriter is single-row only”animation = "typewriter" rejects three combinations: bottom_text set (two-row layout), text_align = "scroll" / "scroll_over" (cursor moves each tick), or empty text. Drop the conflicting setting; typewriter still composes with font_color = "rainbow" and border = "rainbow".
text_align="scroll" needs transparency
Section titled “text_align="scroll" needs transparency”Stretch fills every pixel with image, leaving no transparent regions for the text to scroll through. Use text_align="scroll_over" for a fullscreen marquee, or pair scroll with a transparent .gif and fit="pillarbox".
text_x_offset is rejected in scroll modes
Section titled “text_x_offset is rejected in scroll modes”Static offsets don’t compose with the per-tick scroll cursor. Set text_x_offset = 0 or change text_align to "left" or "right".