Pixi Solid
High-performance 2D scenes, powered by SolidJS reactivity.
At a glance
Section titled “At a glance”Pixi Solid is a library that provides SolidJS components and hooks for working with PixiJS.
- 📦 Full component coverage — Every major PixiJS display object has a corresponding component.
- ⚡ Signals-driven reactivity — State changes automatically update your scene.
- 🧹 Automatic cleanup — Components clean up after themselves on unmount.
- 🧪 Testable without a browser — Context, hooks, and ticker are built for simulation.
Install
Section titled “Install”npm i pixi-solidPeer dependencies of
{ "pixi.js": "^8.14.3", "solid-js": "^1.9.10"}Why pixi-solid?
Section titled “Why pixi-solid?”import { PixiCanvas, Sprite } from "pixi-solid";import { Texture } from "pixi.js";
export const App = () => ( <PixiCanvas style={{ width: "100%", height: "100vh" }} background="#1099bb"> <Sprite texture={Texture.WHITE} x={100} y={100} /> </PixiCanvas>);- Declarative scene graphs
- Compose PixiJS objects with JSX instead of imperative addChild/removeChild calls. The tree is your scene.
- Automatic lifecycle
- Components clean up after themselves on unmount: display objects, event listeners, ticker subscriptions, textures. No manual disposal tracking.
- Shared reactivity
- Signals and stores drive both canvas content and HTML UI from the same state. No bridging layer or two-way sync required.
- Unified timing
- Animations, frame callbacks, and sprite updates all synchronise to the same ticker context. No timing drift between onTick, useSpring, or AnimatedSprite.
- Full PixiJS coverage
- Props, refs, event handlers, and compatibility with plain PixiJS children if you need it.
- Testable by design
- Context providers, hooks, and the ticker are structured for simulation in tests. No browser or canvas required for unit tests.
Code example
Section titled “Code example”import { CRTFilter } from "pixi-filters";import { Container, onTick, PixiCanvas, Sprite, usePixiScreen, TilingSprite } from "pixi-solid";import { objectFit, ObjectFitContainer } from "pixi-solid/utils";import type * as Pixi from "pixi.js";import { Assets, BlurFilter, Rectangle } from "pixi.js";import { createResource, onCleanup, Show } from "solid-js";
import { Character } from "./character";import { Controls } from "./controls";import { createPlayerStore } from "./player-store";import { loadSceneAssets } from "./load-scene-assets";
const DemoScene = (props: { isRunning: boolean; direction: "left" | "right" }) => { const pixiScreen = usePixiScreen(); const sceneBounds = new Rectangle(0, 0, 400, 400);
const blurFilter = new BlurFilter({ strength: 8 }); const crtFilter = new CRTFilter({ curvature: 3, lineContrast: 0.1, vignetting: 0.1, noise: 0.04, noiseSize: 2, });
onTick(({ deltaTime }) => { crtFilter.seed = Math.random() * 10; crtFilter.time += deltaTime * 0.3; });
onCleanup(() => { blurFilter.destroy(); crtFilter.destroy(); });
return ( <ObjectFitContainer width={pixiScreen.width} height={pixiScreen.height} fitMode="cover"> <Container filters={crtFilter} boundsArea={sceneBounds}> <Sprite label="Sky" texture={Assets.get<Pixi.Texture>("sky")} filters={blurFilter} ref={(ref) => { objectFit(ref, sceneBounds, "cover"); }} /> <TilingSprite label="Ground" width={sceneBounds.width} height={sceneBounds.height * 0.3} position={{ x: 0, y: sceneBounds.height * 0.7 }} ref={(tileRef) => { onTick(({ deltaTime }) => { const movementSpeed = props.isRunning ? 1.3 : 0; const directionMultiplier = props.direction === "left" ? 1 : -1; tileRef.tilePosition.x += movementSpeed * directionMultiplier * deltaTime; }); }} texture={Assets.get<Pixi.Texture>("ground")} /> <Character direction={props.direction} isRunning={props.isRunning} position={{ x: sceneBounds.width * 0.5, y: sceneBounds.height * 0.7 }} /> </Container> </ObjectFitContainer> );};
export const DemoApp = () => { const playerStore = createPlayerStore(); const [textureResource] = createResource(loadSceneAssets);
return ( <div style={{ position: "relative" }}> <Controls isRunning={playerStore.state.isRunning} direction={playerStore.state.direction} onToggleDirectionClicked={playerStore.toggleDirection} onToggleRunningClicked={playerStore.toggleRunning} /> <PixiCanvas style={{ "aspect-ratio": "16/9", overflow: "hidden", "border-radius": "10px", }} > <Show when={textureResource()}> <DemoScene isRunning={playerStore.state.isRunning} direction={playerStore.state.direction} /> </Show> </PixiCanvas> </div> );};import { createStore } from "solid-js/store";
export type PlayerStore = { state: Readonly<AppState>; toggleRunning: () => void; toggleDirection: () => void;};
export type AppState = { isRunning: boolean; direction: Direction;};
export type Direction = "left" | "right";
export const createPlayerStore = (): PlayerStore => { const [state, setState] = createStore<AppState>({ isRunning: true, direction: "right", });
const toggleRunning = () => { setState("isRunning", (value) => !value); };
const toggleDirection = () => { setState("direction", (value) => (value === "left" ? "right" : "left")); };
return { state, toggleRunning, toggleDirection, };};import { AnimatedSprite, Container } from "pixi-solid";import type * as Pixi from "pixi.js";import { createEffect, Match, Switch, splitProps } from "solid-js";
import { getIdleAnimationTextures, getRunAnimationTextures } from "./get-animation-textures";
export type CharacterProps = Omit<Pixi.ContainerOptions, "children"> & { direction: "left" | "right"; isRunning: boolean;};
export const Character = (props: CharacterProps) => { const [, pixiProps] = splitProps(props, ["isRunning", "direction"]);
return ( <Container {...pixiProps} label={"Character"}> <Switch> <Match when={props.isRunning}> <AnimatedSprite autoPlay={props.isRunning} ref={(instance) => { createEffect(() => { if (props.isRunning) { instance.play(); } else { instance.stop(); } }); }} textures={getRunAnimationTextures()} scaleX={props.direction === "left" ? -1 : 1} animationSpeed={0.25} anchorX={0.5} anchorY={0.92} /> </Match> <Match when={!props.isRunning}> <AnimatedSprite autoPlay={true} textures={getIdleAnimationTextures()} scaleX={props.direction === "left" ? -1 : 1} animationSpeed={0.25} anchorX={0.5} anchorY={0.92} /> </Match> </Switch> </Container> );};import type * as Pixi from "pixi.js";import { Assets } from "pixi.js";
export const getRunAnimationTextures = () => [ Assets.get<Pixi.Texture>("run_01"), Assets.get<Pixi.Texture>("run_02"), Assets.get<Pixi.Texture>("run_03"), Assets.get<Pixi.Texture>("run_04"), Assets.get<Pixi.Texture>("run_05"), Assets.get<Pixi.Texture>("run_06"),];
export const getIdleAnimationTextures = () => [ Assets.get<Pixi.Texture>("idle_01"), Assets.get<Pixi.Texture>("idle_02"), Assets.get<Pixi.Texture>("idle_03"), Assets.get<Pixi.Texture>("idle_03"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_05"), Assets.get<Pixi.Texture>("idle_06"), Assets.get<Pixi.Texture>("idle_07"), Assets.get<Pixi.Texture>("idle_07"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"),];import { Assets, TextureStyle } from "pixi.js";
import groundAssetUrl from "@/assets/ground-tile.png";import idleAssetUrl_01 from "@/assets/idle_01.png";import idleAssetUrl_02 from "@/assets/idle_02.png";import idleAssetUrl_03 from "@/assets/idle_03.png";import idleAssetUrl_04 from "@/assets/idle_04.png";import idleAssetUrl_05 from "@/assets/idle_05.png";import idleAssetUrl_06 from "@/assets/idle_06.png";import idleAssetUrl_07 from "@/assets/idle_07.png";import idleAssetUrl_08 from "@/assets/idle_08.png";import runAssetUrl_01 from "@/assets/run_01.png";import runAssetUrl_02 from "@/assets/run_02.png";import runAssetUrl_03 from "@/assets/run_03.png";import runAssetUrl_04 from "@/assets/run_04.png";import runAssetUrl_05 from "@/assets/run_05.png";import runAssetUrl_06 from "@/assets/run_06.png";import skyAssetUrl from "@/assets/sky.png";
export const loadSceneAssets = async () => { TextureStyle.defaultOptions.scaleMode = "nearest";
try { await Assets.load([ { alias: "sky", src: skyAssetUrl }, { alias: "ground", src: groundAssetUrl }, { alias: "run_01", src: runAssetUrl_01 }, { alias: "run_02", src: runAssetUrl_02 }, { alias: "run_03", src: runAssetUrl_03 }, { alias: "run_04", src: runAssetUrl_04 }, { alias: "run_05", src: runAssetUrl_05 }, { alias: "run_06", src: runAssetUrl_06 }, { alias: "idle_01", src: idleAssetUrl_01 }, { alias: "idle_02", src: idleAssetUrl_02 }, { alias: "idle_03", src: idleAssetUrl_03 }, { alias: "idle_04", src: idleAssetUrl_04 }, { alias: "idle_05", src: idleAssetUrl_05 }, { alias: "idle_06", src: idleAssetUrl_06 }, { alias: "idle_07", src: idleAssetUrl_07 }, { alias: "idle_08", src: idleAssetUrl_08 }, ]); return true; } catch { return false; }};import style from "./style.module.css";
export type ControlsProps = { isRunning: boolean; direction: "left" | "right"; onToggleRunningClicked: () => void; onToggleDirectionClicked: () => void;};
export const Controls = (props: ControlsProps) => { return ( <div class={style["controls-wrap"]}> <button type="button" class={style["controls-button"]} onClick={props.onToggleRunningClicked}> {props.isRunning ? "Click to stop" : "Click to run"} </button> <br /> <button type="button" class={style["controls-button"]} onClick={props.onToggleDirectionClicked} > {props.direction === "left" ? "Click to face right" : "Click to face left"} </button> </div> );};.controls-wrap { position: absolute; z-index: 1; top: 10px; left: 10px; display: flex; gap: 10px;}
.controls-button { margin: 0; cursor: pointer;}