site logo SVELTE ANIMATED ICON

Imperative Control

Driving animation from code via bind:this and the exported methods.

For most cases trigger, active, and loop are enough. When you need to fire animations from code - outside of mouse events, after an async step, on a keyboard shortcut - bind to the component and call startAnimation() / stopAnimation() directly.

The two methods

AnimatedIcon and every per-icon component expose:

startAnimation(): void;  // runs the configured template now
stopAnimation(): void;   // cancels in-flight animations and clears inline styles

Both are safe to call repeatedly. startAnimation() calls stopAnimation() first so you never see a leftover animation bleed into the new one.

Binding to the component

<script>
  import { Gear } from 'svelte-animated-icon/phosphor';

  let icon = $state();

  function replay() {
    icon.startAnimation();
  }
</script>

<button onclick={replay}>Replay</button>
<Gear bind:this={icon} template="draw" trigger="controlled" />

bind:this on a Svelte 5 component gives you the component instance, not the DOM element. The exported methods (startAnimation, stopAnimation) become instance methods.

Use cases

Replay after an async step

<script>
  import { Gear } from 'svelte-animated-icon/phosphor';
  let icon = $state();
  let saving = $state(false);

  async function save() {
    saving = true;
    await api.save();
    saving = false;
    icon.startAnimation();
  }
</script>

<button onclick={save} disabled={saving}>
  {saving ? 'Saving…' : 'Save'}
  <Gear bind:this={icon} template="tada" trigger="controlled" active={saving} />
</button>

Keyboard shortcut

<svelte:window onkeydown={(e) => e.key === 'r' && icon.startAnimation()} />
<Gear bind:this={icon} template="draw" trigger="controlled" />

Chaining animations

The methods return void, but you can chain by waiting on the underlying Animation objects if you grab them yourself:

<script>
  import { AnimatedIcon, getTemplate } from 'svelte-animated-icon';

  let icon = $state();

  async function doubleAnimation() {
    icon.startAnimation();
    await Promise.all(
      // startAnimation doesn't return animations, but the next mount will.
    );
  }
</script>

For chained sequences, drive each step through trigger="controlled" and toggle active from your state instead. See Parent-Controlled Animation.

Stopping an animation in flight

stopAnimation() cancels the WAAPI animations and calls clearProps() to strip the inline styles. The icon returns to its rest state. Call it from anywhere you have the bound instance:

<script>
  let icon = $state();
</script>

<button onclick={() => icon.stopAnimation()}>Reset</button>
<Gear bind:this={icon} template="draw" trigger="controlled" active={true} />

Why trigger="controlled" for imperative use

If you bind to an icon with trigger="hover", the hover handlers will fire alongside your manual calls - and you can get into weird races where a hover-out cancels an animation you just started.

The clean pattern is:

<Gear bind:this={icon} template="draw" trigger="controlled" />

Now only your code drives it.

Component vs. DOM

bind:this on a Svelte 5 component gives you the component instance (with its exported methods). If you need the underlying <svg> DOM element - for example, to call getBoundingClientRect() - use a wrapper:

<script>
  let wrapper = $state();
  $inspect(() => wrapper?.getBoundingClientRect());
</script>

<div bind:this={wrapper}>
  <Gear template="draw" />
</div>