Skip to content

Effects

An effect is a short visual transition over one or more field areas: fades, sweeps, slides, dissolves, paints, and related kinds. Effects are descriptions in Python. The active runtime lowers them onto the native paint path (terminal and web share the same runtime).

Effects do not replace layout, styling, or hooks. You trigger them when something should animate — often from a hook, after a field update, or once when a panel appears. They need an active runtime and painted field geometry.

Playing effects on fields

The usual entrypoint is BaseGrid.grid_effect. Pass a known kind string (or a built effect instance) and the field names to animate. Field names match the attributes on the grid; during paint those slots are tagged so the runtime can target their rects.

grid_effect
from xnano import BaseGrid, Field, Terminal, on_keyboard

class Panel(BaseGrid, direction="vertical"):
    title: str = Field(default="Hello", height=1, border="rounded")
    body: str = Field(default="Press f to fade this panel.")

    @on_keyboard("f")
    def fade_body(self) -> None:
        self.grid_effect(
            "fade", # (1)!
            color="violet-400", # (2)!
            duration_ms=300,
            fields=["body"], # (3)!
        )

Terminal().run(Panel())
  1. Known kinds are listed under each family below (and as KnownEffectKind in the API).
  2. Effect APIs use color= / background= for color-driven kinds. That is separate from field styling, which prefers foreground= on Field.
  3. fields is required for work to start. An empty list or omit means no effect runs. Multiple names are allowed.

grid_effect returns True when at least one field area was found and the runtime accepted the effect. It returns False when there is no active runtime (for example outside a session) or the fields could not be resolved.

When the first argument is a kind string, keyword arguments mirror Effect(...): duration_ms, color, background, direction, gradient_length, randomness, interpolation, effects, child, times, and key. When the first argument is already an AbstractEffect instance, those kwargs are ignored — set options on the instance itself.

Fades

Fades interpolate color on the target field area.

Kind Class Role
"fade" FadeEffect Fade foreground to a target
"fade_from" FadeFromEffect Fade foreground from a source
"fade_to" FadeToEffect Fade foreground and background to targets
"fade_from_both" FadeFromBothEffect Fade foreground and background from sources
Fade kinds
self.grid_effect(
    "fade",
    color="emerald-400",
    duration_ms=250,
    fields=["body"],
)

self.grid_effect(
    "fade_from",
    color="slate-700",
    duration_ms=250,
    fields=["body"],
)

self.grid_effect(
    "fade_to",
    color="white",
    background="slate-900",
    duration_ms=300,
    fields=["body"],
)

self.grid_effect(
    "fade_from_both",
    color="violet-400",
    background="black",
    duration_ms=300,
    fields=["body"],
)

Typed equivalents:

from xnano.effects import (
    FadeEffect,
    FadeFromBothEffect,
    FadeFromEffect,
    FadeToEffect,
)

FadeEffect(color="emerald-400", duration_ms=250)
FadeFromEffect(color="slate-700", duration_ms=250)
FadeToEffect(color="white", background="slate-900", duration_ms=300)
FadeFromBothEffect(
    color="violet-400",
    background="black",
    duration_ms=300,
)

Dissolve and typewriter coalesce

Cell-level transitions without a motion direction.

Kind Class Role
"dissolve" DissolveEffect Random pixel dissolve
"coalesce" CoalesceEffect Typewriter-style cell assembly
Dissolve and coalesce
self.grid_effect("dissolve", duration_ms=400, fields=["body"])
self.grid_effect("coalesce", duration_ms=500, fields=["body"])
from xnano.effects import CoalesceEffect, DissolveEffect

DissolveEffect(duration_ms=400)
CoalesceEffect(duration_ms=500)

Directional sweeps

Directional sweeps reveal or hide content with a motion gradient.

Kind Class Role
"sweep_in" SweepInEffect Directional sweep revealing content
"sweep_out" SweepOutEffect Directional sweep hiding content

direction is one of "left_to_right", "right_to_left", "up_to_down", "down_to_up". Optional: gradient_length (cells), randomness, and color (gradient accent).

Sweep in and out
self.grid_effect(
    "sweep_in",
    direction="left_to_right",
    gradient_length=14,
    randomness=2,
    color="teal-400",
    duration_ms=400,
    fields=["body"],
)

self.grid_effect(
    "sweep_out",
    direction="up_to_down",
    duration_ms=350,
    fields=["body"],
)
from xnano.effects import SweepInEffect, SweepOutEffect

SweepInEffect(
    direction="left_to_right",
    gradient_length=14,
    randomness=2,
    color="teal-400",
    duration_ms=400,
)
SweepOutEffect(direction="up_to_down", duration_ms=350)

Directional slides

Directional slides, same motion parameters as sweeps.

Kind Class Role
"slide_in" SlideInEffect Directional slide revealing content
"slide_out" SlideOutEffect Directional slide hiding content
Slide in and out
self.grid_effect(
    "slide_in",
    direction="down_to_up",
    duration_ms=350,
    fields=["body"],
)

self.grid_effect(
    "slide_out",
    direction="right_to_left",
    color="sky-300",
    duration_ms=300,
    fields=["body"],
)
from xnano.effects import SlideInEffect, SlideOutEffect

SlideInEffect(direction="down_to_up", duration_ms=350)
SlideOutEffect(
    direction="right_to_left",
    color="sky-300",
    duration_ms=300,
)

Color paints

Paints set colors on the target area (as opposed to fading through them).

Kind Class Role
"paint" PaintEffect Paint foreground and background
"paint_fg" PaintForegroundEffect Paint foreground only
"paint_bg" PaintBackgroundEffect Paint background only
Paint kinds
self.grid_effect(
    "paint",
    color="white",
    background="slate-800",
    duration_ms=200,
    fields=["body"],
)

self.grid_effect(
    "paint_fg",
    color="amber-300",
    duration_ms=200,
    fields=["title"],
)

self.grid_effect(
    "paint_bg",
    background="slate-900",
    duration_ms=200,
    fields=["body"],
)
from xnano.effects import (
    PaintBackgroundEffect,
    PaintEffect,
    PaintForegroundEffect,
)

PaintEffect(color="white", background="slate-800", duration_ms=200)
PaintForegroundEffect(color="amber-300", duration_ms=200)
PaintBackgroundEffect(background="slate-900", duration_ms=200)

Delays and pauses

Kind Class Role
"sleep" SleepEffect Empty delay (useful inside sequences)
"delay" DelayEffect Wait, then run a child
Sleep and delay
from xnano.effects import DelayEffect, Effect, FadeEffect, SleepEffect

# Standalone pause for sequencing
SleepEffect(duration_ms=150)

# Wait, then fade
DelayEffect(
    duration_ms=200,
    child=FadeEffect(color="violet-400", duration_ms=250),
)

# Same idea via kind strings
self.grid_effect(
    "delay",
    duration_ms=200,
    child=Effect("fade", color="violet-400", duration_ms=250),
    fields=["body"],
)

Combining effects

Kind Class Role
"sequence" SequenceEffect Run children one after another (effects=)
"parallel" ParallelEffect Run children together (effects=)
"repeat" RepeatEffect Repeat a child times times (None = forever)
Sequence, parallel, repeat
from xnano.effects import Effect, ParallelEffect, RepeatEffect, SequenceEffect

self.grid_effect(
    "sequence",
    effects=(
        Effect("sweep_in", direction="up_to_down", duration_ms=350),
        Effect("fade", color="sky-300", duration_ms=200),
    ),
    fields=["body"],
)

self.grid_effect(
    "parallel",
    effects=(
        Effect("fade", color="emerald-400", duration_ms=300),
        Effect("paint_bg", background="slate-900", duration_ms=300),
    ),
    fields=["body"],
)

self.grid_effect(
    "repeat",
    child=Effect("fade", color="violet-400", duration_ms=200),
    times=3,
    fields=["title"],
)

Typed composition:

intro = SequenceEffect(
    effects=(
        Effect("sweep_in", direction="left_to_right", duration_ms=400),
        Effect("fade", color="emerald-400", duration_ms=250),
    ),
)
pulse = RepeatEffect(
    child=Effect("fade", color="teal-400", duration_ms=180),
    times=2,
)
together = ParallelEffect(
    effects=(
        Effect("fade_to", color="white", background="black", duration_ms=300),
        Effect("coalesce", duration_ms=300),
    ),
)

self.grid_effect(intro, fields=["title", "body"])

Duration, easing, and filters

Every effect instance carries these base attributes (from AbstractEffect):

Attribute Role
duration_ms Length in milliseconds (default 300)
interpolation Easing curve ("linear", "smooth_step", "sine_in_out", "bounce_out", …)
cell_filter Which cells participate ("all", "text", "non_empty", "background", "background_only")
key Optional id so replaying replaces an in-flight effect on that field

Directional kinds (sweep_*, slide_*) also take:

Attribute Role
direction "left_to_right", "right_to_left", "up_to_down", "down_to_up"
gradient_length Motion gradient length in cells (default 14)
randomness Randomness along the gradient (default 2)
color Accent color of the motion gradient

Notes:

  • duration_ms, interpolation, color, background, direction, gradient_length, randomness, effects, child, times, and key can be passed as kwargs on kind-string calls to grid_effect / Effect(...).
  • cell_filter is set on a typed instance (for example FadeEffect(color="cyan", cell_filter="text")), not as a kind-string kwarg.
  • When effect is already an instance, set key on that instance; the key= kwarg on grid_effect is only used for kind strings.

When effects run

Effects need an active runtime and a painted field geometry, so call grid_effect from hooks, after the grid is attached to a live Terminal / Web session, or from other runtime-bound code — not at module import time.

Trigger from a hook
from xnano import BaseGrid, Field, Terminal, on_keyboard, on_tick

class Banner(BaseGrid, direction="vertical"):
    label: str = Field(default="ready", height=1, border="rounded")
    _intro_done: bool = Field(default=False, state=True)

    @on_tick
    def play_intro_once(self) -> None:
        if self._intro_done:
            return
        self._intro_done = True
        self.grid_effect(
            "coalesce",
            duration_ms=450,
            fields=["label"],
        )

    @on_keyboard("r")
    def replay(self) -> None:
        self.grid_effect(
            "sweep_in",
            direction="left_to_right",
            duration_ms=350,
            fields=["label"],
            key="replay", # (1)!
        )

Terminal().run(Banner())
  1. key de-duplicates: replaying with the same key on the same field replaces the in-flight effect instead of stacking another.

Runtime.play_effect is the lower-level API. Grids wrap it as grid_effect so field names resolve against that grid's layout after paint.

Effects are terminal-native in implementation detail (tachyonfx-backed under xnano-core). On hosts without a live effect engine path, starting an effect may no-op; the description API stays the same.

Kind strings and typed builders

Two equivalent construction paths:

Effect factory vs typed classes
from xnano.effects import Effect, FadeEffect, SweepInEffect

# Factory: kind string + kwargs
fade = Effect("fade", color="emerald-400", duration_ms=250)
sweep = Effect(
    "sweep_in",
    direction="left_to_right",
    duration_ms=400,
)

# Typed classes: same descriptions, explicit types
fade_too = FadeEffect(color="emerald-400", duration_ms=250)
sweep_too = SweepInEffect(direction="left_to_right", duration_ms=400)

Pass either form into grid_effect:

self.grid_effect(fade, fields=["body"])
self.grid_effect("fade", color="emerald-400", fields=["body"])

Prefer kind strings at the call site for one-off triggers. Prefer typed classes when you store, compose, or type-check effect values.

Next

API

xnano.effects · BaseGrid.grid_effect · Runtime.play_effect