Frameworks
Framework integration guides for React and CDN usage.
React
The @sarmal/react package provides a thin React wrapper over @sarmal/core. It gives you a <Sarmal> component for drop-in usage and a useSarmal hook for imperative control.
Installation
npm install @sarmal/core @sarmal/react
The Component
The quickest way to get an animation on screen in React:
import { Sarmal } from "@sarmal/react";
import { rose5 } from "@sarmal/core";
function App() {
return <Sarmal curve={rose5} />;
}
The component renders a <canvas> element. The canvas buffer dimensions must match the display size. You can either pass width and height props directly, or wrap in a parent container with explicit dimensions.
// Explicit dimensions
<Sarmal curve={curves.rose3} width={200} height={200} />
// Or a sized parent
<div style={{ width: 200, height: 200 }}>
<Sarmal curve={curves.rose3} />
</div>
If neither is provided, the canvas falls back to 300x300 with a console warning. width and height are set during initialization. Changing them after mount destroys and recreates the instance.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| curve* | CurveDef | - | Required. The curve definition. Must be a stable reference (module-level const or useMemo). |
| className | string | - | Applied to the <canvas> element |
| style | CSSProperties | - | Applied to the <canvas> element |
| width | number | - | Canvas buffer width in CSS pixels. Init-only. |
| height | number | - | Canvas buffer height in CSS pixels. Init-only. |
| autoStart | boolean | true | Start the animation loop automatically. Init-only. |
| initialPhase | number | undefined | Initial position along the curve. Init-only. |
| skeletonColor | string | '#ffffff' | Hex color for the skeleton path |
| trailColor | string | string[] | '#ffffff' | Hex color or array of hex colors for gradient trails |
| headColor | string | derived | Hex color for the head dot. Omit to auto-follow trail color |
| headRadius | number | 4 | Radius of the head dot in pixels. Changeable at runtime via setRenderOptions. |
| trailLength | number | 120 | Number of trail points. Init-only. |
| trailStyle | TrailStyle | 'default' | 'default' | 'gradient-static' | 'gradient-animated' |
| morphDuration | number | 300 | Duration of the morph transition when curve changes (ms) |
| morphStrategy | 'normalized' | 'raw' | 'normalized' | Phase-mapping strategy for the curve-change morph |
| morphEasing | (t: number) => number | easeInOutCubic | Eases the curve-change morph. Pass (t) => t for a constant-rate ramp |
| morphAlign | boolean | false | Start the new curve from the point nearest the current head, removing the start “snap” |
| onReady | (instance) => void | - | Fired once after the instance is created. Store the reference if you need imperative control. |
Curve stability
The curve prop must be a stable reference.
Use a module-level constant or useMemo:
// [GOOD] module-level constant
import { curves } from "@sarmal/core";
<Sarmal curve={curves.rose3} />
// [GOOD] memoized custom curve
const myCurve = useMemo(
() => ({
name: "circle",
fn: (phase) => ({ x: Math.cos(phase), y: Math.sin(phase) }),
}),
[],
);
<Sarmal curve={myCurve} />
// [BAD] new object every render, causes infinite morph loop
<Sarmal
curve={{
name: "circle",
fn: (phase) => ({ x: Math.cos(phase), y: Math.sin(phase) }),
}}
/>
Curve morphing
When the curve prop changes, it smoothly transitions using morphTo. The morphDuration prop controls how long the transition takes:
const [curve, setCurve] = useState(curves.astroid);
// Later...
setCurve(curves.deltoid); // morphs over 300ms (default)
// Custom duration:
<Sarmal curve={curve} morphDuration={800} />
Imperative control
Use the onReady callback to get the SarmalInstance for manual control:
const sarmalRef = useRef<SarmalInstance | null>(null);
<Sarmal
curve={curves.rose3}
onReady={(inst) => {
sarmalRef.current = inst;
}}
/>;
// Later:
sarmalRef.current?.pause();
sarmalRef.current?.setSpeed(0.5);
useSarmal Hook
For full control over the canvas element and instance lifecycle, you may prefer to use the hook:
import { useSarmal } from "@sarmal/react";
import { curves } from "@sarmal/core";
function MyLoader() {
const { canvasRef, instance } = useSarmal(curves.astroid);
return <canvas ref={canvasRef} style={{ width: 200, height: 200 }} />;
}
The hook returns:
canvasRef: attach to your<canvas>elementinstance: auseRefholding theSarmalInstance. Access withinstance.currentfor imperative calls (play,pause,setSpeed,morphTo, etc.)
Hook signature
function useSarmal(
curve: CurveDef,
options?: Partial<SarmalOptions>,
init?: {
width?: number;
height?: number;
trailLength?: number;
headRadius?: number;
autoStart?: boolean;
initialPhase?: number;
},
morphOptions?: {
morphDuration?: number;
morphStrategy?: "normalized" | "raw";
morphEasing?: (t: number) => number;
morphAlign?: boolean;
},
): {
canvasRef: React.RefObject<HTMLCanvasElement | null>;
instance: React.RefObject<SarmalInstance | null>;
}
Server Side Rendering
@sarmal/react emits "use client" at the top of its bundle output. createSarmal is only ever called inside useEffect, which never runs on the server.
Next.js App Router: Importing @sarmal/react inside a Server Component will fail. Add "use client" to the file that imports it, or wrap the <Sarmal> usage in a dedicated ClientWrapper.tsx that is itself marked "use client".
This also applies when a curve definition is passed as a prop from a server component to a client child. CurveDef contains a function (fn) which React cannot serialize across the server/client boundary. The fix is the same: move the curve import and the component that uses it into a file that has "use client".
// ClientWrapper.tsx
"use client";
import { Sarmal } from "@sarmal/react";
import { curves } from "@sarmal/core";
export default function ClientWrapper() {
return <Sarmal curve={curves.astroid} width={200} height={200} />;
}
SVG Output
For SVG rendering, use <SarmalSVG> and useSarmalSVG. The API mirrors the canvas variants with these differences:
- The
<svg>element has nowidth/heightattributes set. Size it entirely with CSS. - The renderer sets
viewBox="0 0 100 100"automatically, so coordinates are normalized. classNameandstyleapply to the<svg>element.- All init-time props are available:
trailLength,headRadius,autoStart,initialPhase.
// Component
import { SarmalSVG } from "@sarmal/react";
<SarmalSVG curve={curves.rose3} style={{ width: 200, height: 200 }} />
// Hook
import { useSarmalSVG } from "@sarmal/react";
function MyLoader() {
const { svgRef, instance } = useSarmalSVG(curves.astroid);
return <svg ref={svgRef} />;
}
Svelte
The @sarmal/svelte package provides a thin Svelte 5 wrapper over @sarmal/core. It ships three exports per renderer: a component, a composable, and an action.
Installation
npm install @sarmal/core @sarmal/svelte
The Component
The quickest way to get an animation on screen in Svelte 5:
<script>
import { Sarmal } from "@sarmal/svelte";
import { curves } from "@sarmal/core";
</script>
<Sarmal curve={curves.rose3} />
The component renders a <canvas> element. Like the React wrapper, you can pass explicit width and height props or rely on a sized parent container.
<!-- Explicit dimensions -->
<Sarmal curve={curves.rose3} width={200} height={200} />
<!-- Or a sized parent -->
<div style="width: 200px; height: 200px;">
<Sarmal curve={curves.rose3} />
</div>
If neither is provided, the canvas falls back to 300x300 with a console warning. width and height are set during initialization. Changing them after mount destroys and recreates the instance.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| curve* | CurveDef | - | Required. The curve definition. Must be a stable reference (module-level const). |
| class | string | - | Applied to the <canvas> element |
| style | string | - | Applied to the <canvas> element |
| width | number | - | Canvas buffer width in CSS pixels. Init-only. |
| height | number | - | Canvas buffer height in CSS pixels. Init-only. |
| autoStart | boolean | true | Start the animation loop automatically. Init-only. |
| initialPhase | number | undefined | Initial position along the curve. Init-only. |
| skeletonColor | string | '#ffffff' | Hex color for the skeleton path |
| trailColor | string | string[] | '#ffffff' | Hex color or array of hex colors for gradient trails |
| headColor | string | derived | Hex color for the head dot. Omit to auto-follow trail color |
| headRadius | number | 4 | Radius of the head dot in pixels. Init-only. Changeable at runtime via setRenderOptions. |
| trailLength | number | 120 | Number of trail points. Init-only. |
| trailStyle | TrailStyle | 'default' | 'default' | 'gradient-static' | 'gradient-animated' |
| morphDuration | number | 300 | Duration of the morph transition when curve changes (ms) |
| morphStrategy | 'normalized' | 'raw' | 'normalized' | Phase-mapping strategy for the curve-change morph |
| morphEasing | (t: number) => number | easeInOutCubic | Eases the curve-change morph. Pass (t) => t for a constant-rate ramp |
| morphAlign | boolean | false | Start the new curve from the point nearest the current head, removing the start “snap” |
| instance | SarmalInstance | null | - | Bindable. Use bind:instance to get the live SarmalInstance. |
| onready | (instance) => void | - | Callback fired once after the instance is created. |
Curve stability
The curve prop must be a stable reference.
Use a module-level constant:
<script>
import { Sarmal } from "@sarmal/svelte";
import { curves } from "@sarmal/core";
// GOOD: module-level constant
let curve = $state(curves.rose3);
// Later: morph to a different curve
curve = curves.deltoid;
</script>
<Sarmal curve={curve} />
Curve morphing
When the curve prop changes, it smoothly transitions using morphTo. The morphDuration prop controls how long the transition takes:
<script>
import { Sarmal } from "@sarmal/svelte";
import { curves } from "@sarmal/core";
let curve = $state(curves.astroid);
function switchCurve() {
curve = curves.deltoid; // morphs over 300ms (default)
}
</script>
<Sarmal curve={curve} morphDuration={800} />
Imperative control
Use bind:instance or the onready callback to get the SarmalInstance for manual control:
<script>
import { Sarmal } from "@sarmal/svelte";
import { curves } from "@sarmal/core";
import type { SarmalInstance } from "@sarmal/core";
let sarmal = $state<SarmalInstance | null>(null);
function pauseIt() {
sarmal?.pause();
}
</script>
<Sarmal curve={curves.rose3} bind:instance={sarmal} />
The Action
The sarmal action is the most idiomatic Svelte primitive for attaching a sarmal to a <canvas> element. It preserves full access to Svelte’s own directives on the element:
<script>
import { sarmal } from "@sarmal/svelte";
import { curves } from "@sarmal/core";
let options = $state({ curve: curves.rose3, trailColor: "#a78bfa" });
</script>
<canvas width={200} height={200} use:sarmal={options} />
The action responds to option changes via its update callback. Init-time options trigger destroy + recreate, the curve option triggers morphTo, and runtime visual options trigger setRenderOptions.
useSarmal Composable
For full control over the canvas element and instance lifecycle:
<script>
import { useSarmal } from "@sarmal/svelte";
import { curves } from "@sarmal/core";
let canvas = $state<HTMLCanvasElement | null>(null);
const { instance } = useSarmal(
() => canvas,
() => curves.rose3,
);
</script>
<canvas bind:this={canvas} width={200} height={200} />
The composable takes getter functions so it can reactively track changes. The signature:
function useSarmal(
canvasElement: HTMLCanvasElement | null,
getCurve: () => CurveDef,
getOptions?: () => Partial<SarmalOptions>,
getInit?: () => CanvasInit,
getMorphDuration?: () => number | undefined,
): {
get instance(): SarmalInstance | null;
}
SVG Output
For SVG rendering, use <SarmalSVG>, useSarmalSVG, and the sarmalSVG action. The API mirrors the canvas variants:
<!-- Component -->
<script>
import { SarmalSVG } from "@sarmal/svelte";
import { curves } from "@sarmal/core";
</script>
<SarmalSVG curve={curves.rose3} style="width: 200px; height: 200px;" />
<!-- Action -->
<script>
import { sarmalSVG } from "@sarmal/svelte";
import { curves } from "@sarmal/core";
let opts = $state({ curve: curves.astroid, trailColor: "#a78bfa" });
</script>
<svg use:sarmalSVG={opts} style="width: 200px; height: 200px;" />
<!-- Composable -->
<script>
import { useSarmalSVG } from "@sarmal/svelte";
let svg = $state<SVGSVGElement | null>(null);
const { instance } = useSarmalSVG(
() => svg,
() => curves.astroid,
);
</script>
<svg bind:this={svg} style="width: 200px; height: 200px;" />
The SVG renderer sets viewBox="0 0 100 100" automatically. Size the <svg> element with CSS. No width/height props needed as SVG already scales naturally.
SSR and SvelteKit
No special configuration is needed. The $effect rune never runs during server-side rendering, and createSarmal is only called inside $effect blocks. No "use client" directives or typeof window guards are required.
CDN
Drop a sarmal into any HTML page without a build step.
Two approaches:
- auto-init: zero JavaScript
- ESM import: programmatic use
Auto-Init
Auto-init is Sarmal’s plug-n-play option. Include the auto-init script and add data-sarmal attribute to any <canvas> or <svg> with your preferred curve:
<script src="https://unpkg.com/@sarmal/core/dist/auto-init.js"></script>
<canvas data-sarmal="rose3" width="200" height="200"></canvas>
<!-- svg is sized with CSS -->
<svg data-sarmal="rose3" style="width:200px; height:200px;"></svg>
The script waits for DOMContentLoaded, scans for all canvas[data-sarmal] and svg[data-sarmal] elements, and starts an animation for each one using the built-in curve name as the attribute value. Canvas renderers use pixel-space dimensions; SVG renderers set viewBox="0 0 100 100" automatically and are sized with CSS. That’s all you need to get it working. No additional JavaScript required.
You can also use jsDelivr:
<script src="https://cdn.jsdelivr.net/npm/@sarmal/core/dist/auto-init.js"></script>
Data Attributes
Configure appearance and behavior through data-* attributes on the canvas element:
| Attribute | Type | Example | Description |
|---|---|---|---|
| data-sarmal* | string | "rose3" | Required. The built-in curve name. |
| data-trail-color | string | string[] | "#ff6b6b" or '["#ff6b6b","#ffd93d","#6bcb77"]' | Hex color or JSON array for gradient trails |
| data-skeleton-color | string | "#ffffff66" | Hex color for the skeleton ghost path |
| data-head-color | string | "#ff0000" | Hex color for the head dot |
| data-head-radius | number | "6" | Radius of the head dot. Canvas: CSS pixels (default 4). SVG: viewBox units, 0-100 (default 1.5). |
| data-trail-length | number | "60" | Number of trail points |
| data-trail-style | string | "gradient-animated" | "default" | "gradient-static" | "gradient-animated" |
| data-speed | number | "0.5" | Animation speed multiplier. 0.5 = half speed, 2 = double speed |
<canvas
data-sarmal="rose5"
width="300"
height="300"
data-trail-color='["#ff6b6b","#ffd93d","#6bcb77"]'
data-trail-style="gradient-animated"
data-trail-length="80"
data-speed="0.75"
></canvas>
SVG Output
To use svg with auto-init, add data-sarmal to an <svg> element and size it with CSS:
<svg
data-sarmal="rose5"
style="width:300px; height:300px;"
data-trail-color="#ff6b6b"
></svg>
The renderer sets viewBox="0 0 100 100" automatically, so coordinates are normalized. All data-* attributes work identically for both canvas and SVG. The script detects the element type and picks the right renderer.
ESM Import
For full programmatic control, import from jsDelivr as an ES module:
<script type="module">
import { createSarmal } from "https://cdn.jsdelivr.net/npm/@sarmal/core/+esm";
import { rose3 } from "https://cdn.jsdelivr.net/npm/@sarmal/core/+esm";
const canvas = document.getElementById("curve-canvas");
const sarmal = createSarmal(canvas, rose3, {
trailColor: "#a78bfa",
trailLength: 120,
});
</script>
The +esm path resolves to the ESM build and supports named imports. This gives you access to the full API: createSarmal, createSarmalSVG, all curves, palettes, and the createEngine + createRenderer lower-level APIs.
You can also use unpkg (the file paths are the same):
<script type="module">
import { createSarmal } from "https://unpkg.com/@sarmal/core/dist/index.js";
import { rose3 } from "https://unpkg.com/@sarmal/core/dist/index.js";
</script>
SVG with CDN
Combine the ESM import approach with createSarmalSVG:
<svg id="sarmal-svg" style="width: 200px; height: 200px;"></svg>
<script type="module">
import { createSarmalSVG } from "https://cdn.jsdelivr.net/npm/@sarmal/core/+esm";
import { astroid } from "https://cdn.jsdelivr.net/npm/@sarmal/core/+esm";
const svg = document.getElementById("sarmal-svg");
createSarmalSVG(svg, astroid, {
trailColor: "#a78bfa",
});
</script>
The SVG renderer sets viewBox="0 0 100 100" automatically. Size the <svg> element with CSS. You don’t need width or height attributes on the svg element itself.