arwes-svelte Svelte Themes

Arwes Svelte

Svelte 5 port of the excellent https://next.arwes.dev / https://github.com/arwes/arwes

arwes-svelte

Svelte 5 component library wrapping the vanilla ARWES packages. Provides reactive, declarative components for building futuristic sci-fi user interfaces with animation orchestration, sound effects, SVG frames, canvas backgrounds, and visual effects.

How It Works

Architecture

ARWES has two layers:

  1. Vanilla core — framework-agnostic packages (@arwes/animator, @arwes/frames, @arwes/animated, @arwes/bleeps, @arwes/bgs, @arwes/text, @arwes/effects) that expose imperative APIs like createAnimatorSystem(), createFrame(), createAnimatedElement(), etc.

  2. Framework wrappers — the official project provides React wrappers (@arwes/react-*). This library provides the Svelte 5 equivalent.

Each Svelte component wraps exactly one vanilla API. The wrapping pattern is consistent: receive props, create a mutable settings ref, call the vanilla API inside $effect, and return a cleanup function.

The Animator System

The animator is the core orchestration system. It forms a tree of nodes — each <Animator> component registers a node with its parent's system. When the root animator is activated, state transitions flow down the tree: exited → entering → entered on activate, entered → exiting → exited on deactivate.

AnimatorGeneralProvider (global defaults: duration, disabled)
  └── Animator (root, active={true/false})
        ├── Animated (subscribes to parent animator)
        ├── FrameOctagon (subscribes to parent animator)
        ├── Dots (subscribes to parent animator)
        └── Animator (child node, auto-registered)
              └── Text (subscribes to child animator)

How activation flows:

  1. You set active={true} on the root <Animator>.
  2. The Animator component calls node.send(ACTIONS.update) via queueMicrotask.
  3. The vanilla animator system transitions the node: exited → entering → entered.
  4. Child components (<Animated>, <FrameOctagon>, <Dots>, etc.) are subscribed to the node via node.subscribe() and react to state changes.
  5. Child <Animator> nodes receive the transition from their parent and cascade it to their own children.

The Context System

The library uses Svelte's setContext/getContext with a mutable ref pattern:

// In Animator.svelte
const contextRef: AnimatorContextRef = { current: undefined }
setAnimatorContext(contextRef)

// Later, in $effect.pre:
contextRef.current = animatorInterface

Why a mutable .current ref instead of reactive state? Because Svelte context is set once at component initialization. By storing a mutable object, child components that read the context at mount time always access the latest value through .current. State transitions flow through vanilla node.subscribe() callbacks, not context updates.

Three context providers exist:

Provider What it provides Consumer
AnimatorGeneralProvider Default duration, disabled/dismissed flags Animator reads via getAnimatorGeneral()
Animator AnimatorInterface (system + node) Animated, frames, backgrounds, text, bleeps-on-animator, frameAssembler action
BleepsProvider BleepsManager (sound effects) BleepsOnAnimator, getBleeps()

How Each Component Type Wraps Vanilla APIs

Frames (FrameOctagon, FrameCorners, FrameNefrex, etc.)

Each frame component delegates to FrameBase, which calls createFrame(svgElement, settings):

<!-- FrameOctagon.svelte (simplified) -->
<script>
  const settings = $derived(createFrameOctagonSettings({ styled, animated, padding, ... }))
</script>

<FrameBase {settings} class="arwes-frames-frameoctagon">
  {#if children}{@render children()}{/if}
</FrameBase>

FrameBase creates an SVG element, injects the animator node from context into the settings, and calls createFrame(). The vanilla frame renders SVG paths inside the SVG and subscribes to the animator node for enter/exit animations.

9 frame types: FrameOctagon, FrameCorners, FrameNefrex, FrameUnderline, FrameLines, FrameNero, FrameKranox, FrameHeader, FrameCircle.

Animated (animator-driven)

Wraps createAnimatedElement(). Subscribes to the nearest <Animator> and animates CSS properties on state transitions:

<Animated animated="fade">
  <div>This fades in/out with the animator</div>
</Animated>

<Animated animated={{
  initialStyle: { transform: 'translateX(20px)' },
  transitions: {
    entering: { transform: 'translateX(0px)' },
    exiting: { transform: 'translateX(20px)' }
  }
}}>
  <div>This slides in/out</div>
</Animated>

The animated prop accepts a string shorthand ('fade'), an object with initialStyle/transitions, or an array of both.

AnimatedX (state-driven)

Wraps createAnimatedXElement(). Unlike Animated, this does not use the animator — you pass an explicit state prop:

<AnimatedX
  state={currentTab === 'home' ? 'active' : 'inactive'}
  animated={{
    initialStyle: { opacity: 0 },
    transitions: {
      active: { opacity: 1 },
      inactive: { opacity: 0 }
    }
  }}
  hideOnStates={['inactive']}
>
  <div>Content</div>
</AnimatedX>

Useful when animation should be driven by application state rather than the animator tree.

Text

Wraps animateTextSequence() and animateTextDecipher(). Subscribes to the nearest animator and reveals text character-by-character:

<Animator>
  <Text>Arwes futuristic sci-fi UI framework</Text>
</Animator>

Automatically adjusts the animator's duration based on text length (unless fixed is set). Supports a blinking cursor effect and decipher mode.

Backgrounds (Dots, GridLines, MovingLines, Puffs)

Each wraps a canvas-based vanilla API (createBackgroundDots, etc.). The component creates a <canvas>, passes it to the vanilla function along with the animator node, and the vanilla code handles drawing and opacity transitions:

<div style="position:relative;width:300px;height:200px">
  <Dots color="hsl(180 75% 50%)" distance={30} size={2} />
</div>

Canvas backgrounds fade to opacity 1 when the animator enters, and opacity 0 when it exits.

Effects (Illuminator, IlluminatorSVG)

Illuminator wraps createEffectIlluminator() — creates a div-based light effect that follows the mouse cursor. IlluminatorSVG wraps createEffectIlluminatorSVG() — same concept but renders inside an SVG using radial gradients.

<!-- HTML-based illuminator -->
<Illuminator color="hsl(180 75% 50% / 10%)" size={300} />

<!-- SVG-based illuminator -->
<svg bind:this={svg}>
  <IlluminatorSVG {svg} color="hsl(180 75% 50% / 20%)" size={300} />
</svg>

Neither uses the animator — they respond purely to mouse events.

frameAssembler (Svelte action)

Wraps animateFrameAssembler(). This is a Svelte action (not a component) that you apply to an SVG element containing children with data-name="bg", data-name="line", and data-name="deco" attributes. On animator enter/exit, it animates those children with staggered opacity and stroke-dash effects:

<svg use:frameAssembler>
  <rect data-name="bg" ... />
  <line data-name="line" ... />
  <circle data-name="deco" ... />
</svg>

BleepsProvider + BleepsOnAnimator

BleepsProvider wraps createBleepsManager() and provides sound effects to the tree. BleepsOnAnimator subscribes to the nearest animator and plays named bleeps on state transitions:

<BleepsProvider bleeps={{
  click: { sources: [{ src: '/sounds/click.mp3' }] },
  hover: { sources: [{ src: '/sounds/hover.mp3' }] }
}}>
  <Animator active={active}>
    <BleepsOnAnimator transitions={{ entering: 'click' }} />
  </Animator>
</BleepsProvider>

Svelte 5 Patterns Used

Mutable settings ref — Every component that wraps a vanilla API maintains a settingsRef = { current: ... } object. An $effect keeps .current in sync with the latest props. The vanilla API reads from .current on each frame, always getting fresh values without recreating the underlying object.

$effect.pre for structural changes — The Animator component uses $effect.pre (runs before DOM updates) with manual equality checking to avoid recreating animator nodes when unrelated props change. This prevents a Svelte 5 pitfall where rest-destructured $props() cause spurious re-evaluations in $derived.by.

$effect cleanup for subscriptions — Every $effect that creates a vanilla resource returns a cleanup function. This handles both unmount and re-evaluation: return () => resource.cancel().

$derived for computed settings — Frame settings, CSS styles, and attribute objects are computed with $derived or $derived.by, ensuring they update reactively when props change.

queueMicrotask for setup actions — Animator setup and update actions are dispatched via queueMicrotask() to ensure the DOM is ready and context refs are populated before the vanilla system processes them.

Components

Component Vanilla API Purpose
AnimatorGeneralProvider Global animation defaults (duration, disabled)
Animator createAnimatorSystem, system.register Animation orchestration node
Animated createAnimatedElement Animator-driven CSS animation
AnimatedX createAnimatedXElement State-driven CSS animation
Text animateTextSequence, animateTextDecipher Typewriter text reveal
FrameOctagon createFrame + createFrameOctagonSettings Octagonal SVG frame
FrameCorners createFrame + createFrameCornersSettings Corner-accent SVG frame
FrameNefrex createFrame + createFrameNefrexSettings Nefrex-style SVG frame
FrameUnderline createFrame + createFrameUnderlineSettings Underline SVG frame
FrameLines createFrame + createFrameLinesSettings Horizontal lines SVG frame
FrameNero createFrame + createFrameNeroSettings Nero-style SVG frame
FrameKranox createFrame + createFrameKranoxSettings Kranox-style SVG frame
FrameHeader createFrame + createFrameHeaderSettings Header SVG frame
FrameCircle createFrame + createFrameCircleSettings Circular SVG frame
Dots createBackgroundDots Dot pattern canvas background
GridLines createBackgroundGridLines Grid lines canvas background
MovingLines createBackgroundMovingLines Animated lines canvas background
Puffs createBackgroundPuffs Particle puffs canvas background
Illuminator createEffectIlluminator Mouse-following light (HTML)
IlluminatorSVG createEffectIlluminatorSVG Mouse-following light (SVG)
BleepsProvider createBleepsManager Sound effects provider
BleepsOnAnimator Play bleeps on animator transitions

Actions:

Action Vanilla API Purpose
frameAssembler animateFrameAssembler Animate SVG frame assembly on enter/exit

Context helpers:

Function Returns
getAnimator() Current AnimatorInterface
getAnimatorContext() AnimatorContextRef (mutable ref)
getAnimatorGeneral() AnimatorGeneralInterface
getBleepsManager() BleepsManager
getBleeps() Record<string, Bleep>

Usage

<script>
  import {
    AnimatorGeneralProvider,
    Animator,
    Animated,
    Text,
    FrameOctagon,
    Dots,
    BleepsProvider,
    BleepsOnAnimator
  } from 'arwes-svelte'

  let active = $state(false)
</script>

<AnimatorGeneralProvider duration={{ enter: 0.4, exit: 0.4 }}>
  <Animator root {active}>
    <div style="position:relative;width:400px;height:200px">
      <FrameOctagon styled animated />
      <Dots color="hsl(180 75% 50%)" distance={20} size={2} />
    </div>

    <Animated animated="fade">
      <Animator>
        <Text>Welcome to the future</Text>
      </Animator>
    </Animated>
  </Animator>
</AnimatorGeneralProvider>

<button onclick={() => active = !active}>
  {active ? 'Deactivate' : 'Activate'}
</button>

Demo Pages

Start the dev server and visit:

npm run dev -- --port 5179

Testing

123 Playwright tests compare the Svelte components against vanilla ARWES APIs to verify identical behavior — screenshot comparisons, CSS property assertions, canvas pixel checks, and DOM structure validation.

npm run test:e2e                  # Run all tests
npm run test:e2e:update           # Update screenshot baselines
npx playwright test --ui          # Interactive test runner

Top categories

Loading Svelte Themes