Animations
@gtkx/animated adapts React Spring to GTKX. Learn how to create and control springs in the React Spring docs; this guide covers how those values reach native GTKX elements. Install the package separately:
npm install @gtkx/animated@betaAnimated components
animated(Component) wraps a GTKX element so its mutable props accept values from React Spring. Create the wrapper at module scope, then pass it the values a hook returns:
import { animated, useSpring } from "@gtkx/animated";
import { GtkLabel } from "@gtkx/jsx/gtk";
const AnimatedLabel = animated(GtkLabel);
export const FadeIn = () => {
const styles = useSpring({ from: { opacity: 0 }, to: { opacity: 1 } });
return <AnimatedLabel opacity={styles.opacity} label="Hello" />;
};Wrap generated elements, non-widget objects such as GtkAdjustment, or your own components. A module-scoped wrapper keeps its component identity between renders, and production builds retain only the elements the app uses.
Each frame, the current values are written straight onto the widget through its ref, so the component does not re-render while the spring runs. Writable GObject props such as opacity, margins, size requests, and adjustment values can be animated this way. Construct-only props remain static, and TypeScript rejects springs passed to them. Labels and text children also accept animated values:
const { count } = useSpring({ from: { count: 0 }, to: { count: 100 } });
<AnimatedLabel label={count.to((value) => `${Math.round(value)}%`)} />;GTKX truncates whole-number properties, such as margins, toward zero and clamps values to the native property range. An opacity spring cannot exceed 1, and margins cannot be negative. To slide a widget using a margin, animate from a positive margin to zero.
The wrapper passes a ref through. A component of your own that forwards it to the widget it renders gets the same per-frame writes, while one that keeps the ref re-renders with the current values instead.
Props that are not GObject properties, such as accessible*, animate by re-rendering the component on each frame. style is handled specially: animated declarations update the shared stylesheet imperatively, without re-rendering the component.
Animated styles
GTK4 has no inline styles, so the style prop serializes its object into a rule in GTKX's shared Gtk.CssProvider. A spring can drive the whole object or one declaration, and updates made during the same animation turn are applied with one provider reload:
import { animated, useSpring } from "@gtkx/animated";
import { GtkLabel } from "@gtkx/jsx/gtk";
const AnimatedLabel = animated(GtkLabel);
export const Deadline = ({ isOverdue }: { isOverdue: boolean }) => {
const { level } = useSpring({ level: isOverdue ? 1 : 0 });
return (
<AnimatedLabel
label="Due today"
style={level.to((value) => ({ color: `mix(var(--window-fg-color), var(--error-color), ${value})` }))}
/>
);
};The spring carries a number and the interpolation builds the declaration around it. A spring returned by useSpring can also be passed directly as the style object or placed on one declaration:
const styles = useSpring({ from: { color: "red" }, to: { color: "blue" } });
<AnimatedLabel style={styles} label="Due today" />;
<AnimatedLabel style={{ color: styles.color, paddingTop: 4 }} label="Due today" />;Nested blocks work too, so style={{ "&:hover": { color: styles.color } }} is animated without a React render. Only style is read this way; a spring nested inside another object-valued prop is not tracked.
A provider reload invalidates styling across the display. Prefer a native property for effects it can express, or a CSS transition or @keyframes for a fixed state change. Use animated style for dynamic CSS values, and profile large trees with many animations. For custom drawing, use GtkDrawingArea and call queueDraw() as values change.
Moving widgets
GTK positions widgets through layout, so a position is animated where a container lets you set one. Inside a GtkFixed, the transform of a GtkFixedLayoutChild is a Gsk.Transform, and an interpolation builds one from the spring:
import { animated, useSpring } from "@gtkx/animated";
import * as Graphene from "@gtkx/gi/graphene";
import * as Gsk from "@gtkx/gi/gsk";
import { GtkButton, GtkFixed, GtkFixedLayoutChild } from "@gtkx/jsx/gtk";
const AnimatedFixedLayoutChild = animated(GtkFixedLayoutChild);
const translate = (x: number): Gsk.Transform | null => Gsk.Transform.new().translate(new Graphene.Point({ x, y: 0 }));
export const Slide = ({ isOpen }: { isOpen: boolean }) => {
const { x } = useSpring({ x: isOpen ? 240 : 0 });
return (
<GtkFixed>
<AnimatedFixedLayoutChild transform={x.to(translate)}>
<GtkButton label="Slide" />
</AnimatedFixedLayoutChild>
</GtkFixed>
);
};Elsewhere, animate the margins, the size requests, a Gtk.Paned's position, or a CSS transform through style, which moves what the widget paints without disturbing the layout around it.
React Spring APIs
@gtkx/animated re-exports React Spring's platform-neutral APIs. Use the React Spring documentation to choose and configure hooks, transitions, and controllers, then pass their animated values to GTKX props as shown above. Browser hooks that depend on the DOM have no GTK counterpart and are not exported.
The frame clock
Frames advance during the update phase of a mapped window's GTK frame clock, before layout and painting. GTKX keeps using that window while it can supply frames, switching to another mapped window or a timer when necessary.
Reduced motion
GTK exposes two settings with different effects:
gtk-enable-animationsdisables motion across the toolkit. When false, GTKX springs jump to their targets while still running their lifecycle callbacks.gtk-interface-reduced-motion, available in GTK 4.22 and later, asks applications to reduce motion. It also drives GTK'sprefers-reduced-motionmedia query. Springs keep running; the component chooses an alternative, such as a fade instead of a slide.
useReducedMotion() returns true when either preference calls for less motion, false otherwise, and null before a display is open. It updates when the settings change.
@gtkx/testing disables animations by default, so springs reach their targets on the first frame. Assert the result with findBy* or waitFor. Pass areAnimationsEnabled: true to render when the test needs to exercise motion.
Next
Continue with Cairo to draw 2D graphics inside a widget.