Spine Web Components

The spine-webcomponents package provides web components (custom HTML elements) for embedding Spine animations directly into a web page.

When the <spine-skeleton> tag is added to an HTML page, the library creates a shared WebGL canvas overlay where multiple Spine skeletons are rendered. This design overcomes browser limitations on the number of WebGL contexts.

Unlike the Spine player, the web components do not include a built-in UI for controlling playback. Instead, they expose a wide range of attributes that enable fine-grained configuration directly through HTML.

Exporting

Spine web components use the same export format as the Spine player. In addition, it supports using multiple skeletons in the same JSON file.

Set up

Adding a <spine-skeleton> web component to a website involves only a few straightforward steps, outlined below.

Adding the JavaScript

The spine-webcomponents package includes the JavaScript file spine-webcomponents.js, which defines the two HTML custom elements <spine-skeleton> and <spine-overlay>, along with a set of related utility functions.

<script src="https://unpkg.com/@esotericsoftware/spine-webcomponents@4.2.*/dist/iife/spine-webcomponents.js"></script>

In the above example, the the file is loaded from UNPKG, a fast NPM CDN. The URL contains a version number (4.2) which must match the Spine editor version used to export the skeleton. The asterisk (*) for the patch version ensures the latest JavaScript code for the major.minor version.

Use the .min.js file extension for a minified file from the UNPKG CDN:

<script src="https://unpkg.com/@esotericsoftware/spine-webcomponents@4.2.*/dist/iife/spine-webcomponents.min.js"></script>

Alternatively, the file can be self-hosted by downloading it from UNPKG or by building it from the sources available on the GitHub repository. The repository also includes instructions for using the spine-webcomponents with NPM or Yarn.

Using a spine-skeleton

After importing the JavaScript file, the web component can be used directly in HTML without additional JavaScript:

html
<spine-skeleton
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
></spine-skeleton>

The <spine-skeleton> element loads the skeleton data from /files/spineboy/export/spineboy-pro.skel and the atlas from /files/spineboy/export/spineboy.atlas. The atlas references an image file (spineboy.png), which is loaded relative to the .atlas file, from /files/spineboy/export/spineboy.png.

A <spine-overlay> element is automatically added at the bottom of the DOM. This component creates a transparent WebGL canvas that spans the entire page and is used to render all <spine-skeleton> components in the correct positions within their parent containers. Most users don't need to be concerned with the overlay.

The web component renders the skeleton scaled to fit its parent element.

1 2
3 4
html
<table>
   <tr>
      <td>1</td>
      <td>2
         <spine-skeleton
            atlas="/files/spine-widget/assets/spineboy-pma.atlas"
            skeleton="/files/spine-widget/assets/spineboy-pro.skel">
         </spine-skeleton>
    </td>
   </tr>
   <tr>
      <td>3
         <spine-skeleton
            atlas="/files/spine-widget/assets/spineboy-pma.atlas"
            skeleton="/files/spine-widget/assets/spineboy-pro.skel">
         </spine-skeleton>
      </td>
      <td>4</td>
   </tr>
</table>!!

Configuration

The <spine-skeleton> element provides many configuration attributes, allowing it to be tailored to specific requirements.

JSON, binary, and atlas URL

The two mandatory attributes, skeleton and atlas, define the source paths for the skeleton .json or binary .skel file and the .atlas file, respectively. These paths can be either relative or absolute URLs.

html
<spine-skeleton
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel">
</spine-skeleton>

<spine-skeleton
   atlas="https://esotericsoftware.com/assets/spineboy-pma.atlas"
   skeleton="https://esotericsoftware.com/assets/spineboy-pro.skel">
</spine-skeleton>

When using absolute URLs to another domain, it is possible that web browsers won't be able to load the assets. This can be solved by enabling CORS on the server that hosts the assets.

Embedding data

Instead of loading data from URLs, the .json/.skel, .atlas, and .png files can be embedded directly using the raw-data attribute. This attribute accepts a stringified JSON object where keys are asset names and values are their Base64-encoded contents. The skeleton and atlas attributes should then reference the corresponding asset names used in this object. The raw-data attribute is used to enable this setup.

html
<spine-skeleton
   atlas="/assets/inline.atlas"
   skeleton="/assets/inline.skel"
   animation="animation"
   raw-data='{
      "/assets/inline.atlas":"aW5saW5lLnBuZwpzaXplOjE2LDE2CmZpbHRlcjpMaW5lYXIsTGluZWFyCnBtYTp0cnVlCmRvdApib3VuZHM6MCwwLDEsMQo=",
      "/assets/inline.skel":"/B8S/IqaXgYHNC4yLjM5wkgAAMJIAABCyAAAQsgAAELIAAAAAQRkb3QCBXJvb3QAAAAAAAAAAAAAAAA/gAAAP4AAAAAAAAAAAAAAAAAAAAAABGRvdAAAAAAAAAAAAAAAAABCyAAAQsgAAAAAAAAAAAAAAAAAAAAAAQRkb3QB//////////8BAAAAAAABAAEBACWwfdcAAAAAP4AAAD+AAAA/gAAAP4AAAAAAAQphbmltYXRpb24BAQABAQMAAAAAAP////8/gAAA/wAA/wBAAAAA/////wAAAAAAAAAAAA==",
      "/assets/inline.png":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRF////p8QbyAAAAApJREFUeJxjZAAAAAQAAiFkrWoAAAAASUVORK5CYII="
   }'

></spine-skeleton>

Style, width, height

By default, the web component renders the skeleton to fill its parent's size, but its actual dimensions are zero width and height. To manually set the size of the web component, use the standard style or class attributes. These attributes can be used to apply any desired styles.

html
<style>
   .custom-class {
      width: 150px;
      height: 150px;
      border: 1px solid green;
      border-radius: 10px;
      box-shadow: -5px 5px 3px rgba(0, 255, 0, 0.3);
      margin-right: 10px;
   }
</style>

<spine-skeleton
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   animation="walk"
   class="custom-class"
></spine-skeleton>

<spine-skeleton
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   animation="walk"
   style="
      width: 150px;
      height: 150px;
      border: 1px solid red;
      border-radius: 10px;
      box-shadow: -5px 5px 3px rgba(255, 0, 0, 0.3);
   "

></spine-skeleton>

JSON skeleton key

To minimize requests for resources, multiple skeletons can be embedded in a single JSON file. When using such JSON, specify which skeleton to display by setting the json-skeleton-key attribute on the web component. The spine-webcomponents asset manager efficiently loads each asset only once, even if used on the page multiple times.

html
<spine-skeleton
   atlas="/files/spine-widget/assets/atlas2.atlas"
   skeleton="/files/spine-widget/assets/demos.json"
   json-skeleton-key="armorgirl"
   animation="animation"
></spine-skeleton>

<spine-skeleton
   atlas="/files/spine-widget/assets/atlas2.atlas"
   skeleton="/files/spine-widget/assets/demos.json"
   json-skeleton-key="greengirl"
   animation="animation"
></spine-skeleton>

Animation

By default, the web component will show the setup pose. An animation can be set using the animation attribute:

html
<spine-skeleton
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   animation="walk"
></spine-skeleton>

Default skin

By default, the web component will use the default skin of the skeleton, which only has attachments that are not in a skin in the Spine editor. The active skin can be set explicitly in the configuration via the skin property:

html
<spine-skeleton
   atlas="/files/spine-widget/assets/mix-and-match-pma.atlas"
   skeleton="/files/spine-widget/assets/mix-and-match-pro.skel"
   animation="dance"
   skin="full-skins/girl-spring-dress"
></spine-skeleton>

skin accepts a comma-separated list of skin names. The skins will be combined into a new one, in the order provided. If multiple skins affect the same slot, the last one in the list takes precedence.

html
<spine-skeleton
   atlas="/files/spine-widget/assets/mix-and-match-pma.atlas"
   skeleton="/files/spine-widget/assets/mix-and-match-pro.skel"
   animation="dance"
   skin="nose/short,skin-base,eyes/violet,hair/brown,clothes/hoodie-orange,legs/pants-jeans,accessories/bag,accessories/hat-red-yellow,eyelids/girly"
></spine-skeleton>

Fit mode

The web component tries to fit the skeleton animation within its container element depending on the fit attribute. Here are some examples:

contain: as large as possible while still containing the skeleton entirely within the container element (Default)
fill: fill the container element by distorting the skeleton's aspect ratio
scaleDown: scale the skeleton down to ensure that the skeleton fits within the container element
none: display the skeleton without regard to the container element size (here the [scale](#scale) attribute is set to `0.05`).

Additional fit modes are:

  • width: fill the container element width, regardless of whether the skeleton overflows the container element vertically.
  • height: fill the container element height, regardless of whether the skeleton overflows the container element horizontally.
  • cover: as small as possible while still covering the entire container element.
  • origin: the skeleton origin is centered with the container element regardless of the bounds.

Bounds

The web component uses the skeleton bounds to fit inside the container element. The skeleton bounds are the bounding box (AABB) of the animation (or multiple animations), or of the setup pose if no animation is specified. To ensure the bounds fit the parent element according to the specified fit mode, the skeleton's scaleX and scaleY are set. This means these properties cannot be changed manually. To control scaleX and scaleY directly, set fit="none" or fit="origin" and access the skeleton object via JavaScript as explained below.

Scale

The Skeleton loader scale is set through the scale attribute. Read more about scaling in the Spine Runtimes Guide.

In this example we set the fit mode to none to effectively view the scale change (otherwise scaleX and scaleY would be modified using the default fit mode).

scale="0.3"
scale="0.2"
scale="0.1"

Axis

Use x-axis and y-axis to shift the skeleton horizontally or vertically by percentage of the container element's width and height.

fit="none"
scale=".2"
x-axis=".25"
fit="origin"
scale=".2"
fit="origin"
scale=".2"
y-axis="-.5"

Offset

Use offset-x and offset-y to shift the skeleton horizontally or vertically by the specified number of pixels.

offset-x="0"
offset-y="0"
offset-x="-100"
offset-y="50"

Padding

Add virtual padding to the container element using pad-left, pad-right, pad-top, and pad-bottom. These values are percentages of the container's width for left and right, and percentages of the container's height for top and bottom.

pad-left="0" pad-right="0" pad-top="0" pad-top="0"
pad-left=".25" pad-right=".25" pad-top=".25" pad-top=".25"

Identifier

Assign an identifier to the web component to retrieve it using the spine.getSkeleton function. This makes it easy to access the Skeleton and AnimationState objects in JavaScript code.

<spine-skeleton> executes some asynchronous operations to retrieve assets. The whenReady method is used to know when to access the Skeleton and AnimationState objects.

html
<spine-skeleton
   atlas="/files/spine-widget/assets/raptor-pma.atlas"
   skeleton="/files/spine-widget/assets/raptor-pro.skel"
   identifier="raptor"
></spine-skeleton>
js
const raptor = await spine.getSkeleton("raptor").whenReady;
raptor.skeleton.color.set(1, 0, 0, 1);

Clip

The clip attribute will hide everything that is outside the container element.

Beware that this will break batching across skeletons.

html
<spine-skeleton
   atlas="/files/spine-widget/assets/tank-pma.atlas"
   skeleton="/files/spine-widget/assets/tank-pro.skel"
   animation="drive"
   fit="height"
   pad-top="0.3"
   pad-bottom="0.3"
></spine-skeleton>

<spine-skeleton
   atlas="/files/spine-widget/assets/tank-pma.atlas"
   skeleton="/files/spine-widget/assets/tank-pro.skel"
   animation="drive"
   fit="height"
   pad-top="0.3"
   pad-bottom="0.3"
   clip
></spine-skeleton>

Custom bounds

Custom bounds can be specified to focus on specific details of the animation, to zoom out, simulate camera movement, etc.

The bounds-x, bounds-y, bounds-width, and bounds-height attributes define custom bounds.

This example focuses on Celeste's face. To prevent the skeleton from overflowing the container element, we set the clip attribute.

html
<spine-skeleton
   atlas="/files/spine-widget/assets/celestial-circus-pma.atlas"
   skeleton="/files/spine-widget/assets/celestial-circus-pro.skel"
   animation="wings-and-feet"
   bounds-x="-155"
   bounds-y="650"
   bounds-width="300"
   bounds-height="350"
   clip
></spine-skeleton>

Auto calculate bounds

The animation can be changed by modifying the animation attribute. The web component will switch to the new animation as if it were freshly created. However, new bounds are not recalculated unless the auto-calculate-bounds attribute is set. This default behavior helps maintain consistent skeleton dimensions across animations. It might be useful to combine it with the animation-bounds attribute.

No auto-calculate-bounds set
auto-calculate-bounds
html
<spine-skeleton
   identifier="spineboy-auto-bounds-1"
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   animation="jump"
></spine-skeleton>

<spine-skeleton
   identifier="spineboy-auto-bounds-2"
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   animation="jump"
   auto-calculate-bounds
></spine-skeleton>
js
const [wc1, wc2] = await Promise.all([
   spine.getSkeleton("spineboy-auto-bounds-1").whenReady,
   spine.getSkeleton("spineboy-auto-bounds-2").whenReady,
]);
let toogleAnimation = false;
setInterval(() => {
   const newAnimation = toogleAnimation ? "jump" : "death";
   wc1.setAttribute("animation", newAnimation)
   wc2.setAttribute("animation", newAnimation)
   toogleAnimation = !toogleAnimation;
}, 4000);

Default mix

The default-mix attribute defines the default mix duration for the AnimationState. This is the default time in seconds to mix between animations when the animation changes, whether by using the animations attribute or by JavaScript code using the AnimationState object.

default-mix="0" (default)
default-mix="1"
html
<spine-skeleton
   identifier="spineboy-default-mix-1"
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   animation="idle"
   default-mix="0"
></spine-skeleton>
<spine-skeleton
   identifier="spineboy-default-mix-2"
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   animation="idle"
   default-mix="1"
></spine-skeleton>
js
const [wc1, wc2] = await Promise.all([
   spine.getSkeleton("spineboy-default-mix-1").whenReady,
   spine.getSkeleton("spineboy-default-mix-2").whenReady,
]);
let toogleAnimation = false;
setInterval(() => {
   const newAnimation = toogleAnimation ? "idle" : "run";
   wc1.setAttribute("animation", newAnimation)
   wc2.setAttribute("animation", newAnimation)
   toogleAnimation = !toogleAnimation;
}, 4000);

Animations

To display a sequence of animations without code, the animations attribute can be used. Here we reproduced the example above with just web component attributes:

html
<spine-skeleton
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   animation-bounds="walk,run"
   default-mix="1"
   animations="
      [loop, 0, 3.5]
      [0, idle, true]
      [0, run, true, 4]
   "

></spine-skeleton>

The animations attribute accepts a string composed of groups enclosed in square brackets, for example: [...][...][...].

Each group defines an animation to be played, with parameters provided as a comma-separated list:

  • track: the track number on which the animation will play
  • animation name: the name of the animation
  • loop: true to loop the animation (false if omitted)
  • delay: seconds to wait after the previous animation starts, or 0 to wait until the previous animation ends (0 if omitted)
  • mixDuration: seconds to mix from the previous animation to this animation (default-mix if omitted, not applicable for the first animation on a track)

To enable looping of a track after the last animation, a special group [loop, trackNumber, repeatDelay] can be added, where:

  • loop: identifies this as a loop instruction
  • trackNumber: the track number to loop
  • repeatDelay: the number of seconds to wait after the last animation is completed before repeating the loop (0 if omitted)

The first group for each track number is passed to the setAnimation method. Subsequent groups for the same track are passed to addAnimation.

To use setEmptyAnimation or addEmptyAnimation, the animation name #EMPTY# must be specified. In this case, the loop parameter is ignored.

Refer to the two examples below for clarification.

Spineboy uses this value for the animations attribute:

[loop, 0]
[0, idle, true]
[0, run, false, 2, 0.25]
[0, run]
[0, run]
[0, run-to-idle, false, 0, 0.15]
[0, idle, true]
[0, jump, false, 0, 0.15]
[0, walk, false, 0, 0.05]
[0, death, false, 0, 0.05]

All animations are played on a single track. Here's a breakdown of the sequence:

  • [loop, 0]: instructs track 0 to loop back to the beginning upon reaching the end
  • [0, idle, true]: sets the idle animation to loop
  • [0, run, false, 2, 0.25]: queues the run animation to start after 2 seconds with a 0.25 second mix
  • [0, run]: queues an additional run animation, without looping
  • [0, run]: queues another run animation
  • [0, run-to-idle, false, 0, 0.15]: queues the run-to-idle transition with no delay and a 0.15 second mix
  • [0, idle, true]: queues the idle animation to loop again
  • [0, jump, false, 0, 0.15]: queues the jump animation with no delay and a 0.15 second mix
  • [0, walk, false, 0, 0.05]: queues the walk animation with no delay and a 0.05 second mix
  • [0, death, false, 0, 0.05]: queues the death animation with no delay and a 0.05 second mix

Celeste uses the following value for the animations attribute:

[0, wings-and-feet, true]
[loop, 1]
[1, #EMPTY#]
[1, eyeblink, false, 2]

This example uses two tracks. Track 0 plays the wings-and-feet animation. Track 1 loops, playing an empty animation followed by the eyeblink animation with a 2 second delay.

The textarea above can be modified for experimentation, then click Update animation. For example, changing the delay from 2 to 0.5 results in more frequent blinking. To start the swing animation on track 0 after 5 seconds with a 0.5 second mix, append: [0, swing, true, 5, 0.5]

Animations bounds

To define bounds based on multiple animations, the animation-bounds attribute can be used. This attribute accepts a list of animations and calculates bounds that encompass all of them.

This approach helps maintain a consistent scale across animations and prevents the skeleton from overflowing its container when switching to an animation with larger bounds.

No animation-bounds
animation-bounds="walk,jump"

Spinner

spinner attribute allows to show a spinner while assets are loading. By default, nothing is shown during assets load. Click the buttons below to simulate a 1 second loading delay and to toggle the spinner attribute.

html
<spine-skeleton
   identifier="spineboy-loading"
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel"
   spinner
></spine-skeleton>

<input type="button" value="Toggle spinner: OFF" onclick="toggleSpinner(this)" />
<input type="
button" value="Reload" onclick="reloadWidget(this)" />
js
const wcLoading = spine.getSkeleton("spineboy-loading");
async function reloadWidget(element) {
   element.disabled = true;
   await wcLoading.whenReady;
   wcLoading.loading = true;
   setTimeout(() => {
   element.disabled = false;
   wcLoading.loading = false;
   }, 1000)
}
function toggleSpinner(element) {
   wcLoading.spinner = !wcLoading.spinner;
   element.value = wcLoading.spinner ? "Toggle spinner: OFF" : "Toggle spinner: ON";
}

Offscreen behavior

Web components that are off-screen are not rendered. While off-screen, by default the AnimationState update, Skeleton update, skeleton.apply, and skeleton.updateWorldTransform functions are not invoked. This corresponds to offscreen=pause.

To ensure that update functions are invoked even when off-screen, set offscreen=update.

To ensure that all functions are invoked regardless of visibility, set offscreen=pose.

pause
update
pose

When this page is refreshed with all three skeletons visible in the viewport, their animations start in sync. However, the first skeleton has offscreen="pause", which causes its state to pause when scrolled out of view. Upon returning to the viewport, its animation resumes without advancing the paused time, resulting in desynchronization with the other two skeletons. While the other two skeletons remain in sync, slight differences may occur, particularly when physics is involved.

To prevent a skeleton from being paused while off-screen, it is recommended to use the update behavior. This avoids invoking updateWorldTransform, which is typically the most CPU-intensive function.

Custom update

A web component's skeleton and state are updated and applied similarly to other runtimes.

Custom logic can be injected before and after updateWorldTransform is called by setting beforeUpdateWorldTransforms and afterUpdateWorldTransforms, respectively.

To fully replace the default update behavior, assign a function to the update property. This replaces both state and skeleton updates, including off-screen optimizations. In this case, managing the update, apply, and updateWorldTransform invocation becomes the developer's responsibility. The onScreen property, which is true when the component is visible on screen, can be used for covenience.

All three functions follow the same signature: (delta: number, skeleton: Skeleton, state: AnimationState) => void

Drag

Setting the drag attribute enables dragging for the web component. This may increase CPU usage, so it is advisable to use this feature only when draggable behavior is required.

Pointer position

To determine the pointer position in various coordinate spaces, the following properties can be used:

For spine-skeleton:

  • pointerWorldX and pointerWorldY: the x and y coordinates of the pointer relative to the skeleton origin (Spine world coordinates).
  • worldX and worldY: the x and y coordinates of the skeleton origin relative to the canvas/WebGL context origin (Spine world coordinates).

For spine-overlay:

  • pointerCanvasX and pointerCanvasY: the x and y coordinates of the pointer relative to the top-left corner of the canvas (screen coordinates).
  • pointerWorldX and pointerWorldY: the x and y coordinates of the pointer relative to the canvas/WebGL context origin (Spine world coordinates).

These properties enable interactive behavior with the web component. For example, in the examples below the owl's eyes follow the pointer.

html
<spine-skeleton
   identifier="owl-pointer"
   atlas="/files/spine-widget/assets/owl-pma.atlas"
   skeleton="/files/spine-widget/assets/owl-pro.skel"
   animations="[0, idle, true][1, blink, true]"
></spine-skeleton>
js
const wc = await spine.getSkeleton("owl-pointer").whenReady;
const controlBone = wc.skeleton.findBone("control");
const tempVector = new spine.Vector3();
wc.afterUpdateWorldTransforms = () => {
   controlBone.parent.worldToLocal(tempVector.set(wc.pointerWorldX, wc.pointerWorldY));
   controlBone.x = controlBone.data.x + tempVector.x / wc.overlay.canvas.width * 30;
   controlBone.y = controlBone.data.y + tempVector.y / wc.overlay.canvas.height * 30;
}

Interaction callbacks

Callbacks can be attached to the web components to handle pointer interactions by setting the interactive attribute.

Callbacks can respond to interactions either within the web component's bounds or with specific slots. Supported events (PointerEventType) include down, up, enter, leave, move, and drag.

To add callbacks:

  • Set pointerEventCallback: (event: PointerEventType) => void to handle pointer actions within the web component bounds.
  • Call addPointerSlotEventCallback (slotRef: number | string | Slot, slotFunction: (slot: Slot, event: PointerEventType) => void) to handle pointer actions within the attachment bounds of the specified slot.

In the example below:

  • pointerEventCallback triggers the jump animation on enter, and the wave animation on leave.
  • addPointerSlotEventCallback adds a callback for the head-base slot (the face). When the attachment receives a down event, the normal and dark tint are updated based on the selected colors in the two tint selectors.

Tint normal:
Tint black:
html
<spine-skeleton
   identifier="interactive0"
   atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
   skeleton="/files/spine-widget/assets/chibi-stickers.skel"
   skin="mario"
   animation="emotes/wave"
   animation-bounds="emotes/wave,emotes/hooray"
   pages="0,4"
   interactive
></spine-skeleton>

<spine-skeleton
   identifier="interactive1"
   atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
   skeleton="/files/spine-widget/assets/chibi-stickers.skel"
   skin="nate"
   animation="emotes/wave"
   animation-bounds="emotes/wave,emotes/hooray"
   pages="0,6"
   interactive
></spine-skeleton>

Tint normal: <input type="color" id="color-picker" value="#ff0000" style="margin: 0;" />
Tint black: <input type="color" id="dark-picker" value="#000000" style="margin: 0;"/>
js
const colorPicker = document.getElementById("color-picker");
const darkPicker = document.getElementById("dark-picker");
[0, 1].forEach(async (i) => {
   const wc = await spine.getSkeleton(`interactive${i}`).whenReady;
   wc.pointerEventCallback = (event) => {
      if (event === "enter") wc.state.setAnimation(0, "emotes/hooray", true).mixDuration = .15;
      if (event === "leave") wc.state.setAnimation(0, "emotes/wave", true).mixDuration = .25;
   }
   const tempColor = new spine.Color();
   const slot = wc.skeleton.findSlot("head-base");
   slot.darkColor = new spine.Color(0, 0, 0, 1);
   wc.addPointerSlotEventCallback(slot, (slot, event) => {
      if (event === "down") {
      slot.darkColor.setFromColor(spine.Color.fromString(darkPicker.value, tempColor));
      slot.color.setFromColor(spine.Color.fromString(colorPicker.value, tempColor));
      }
   });
})

Debug mode

Debug mode can be enabled by setting the debug attribute. This displays the following visual indicators:

  • The skeleton's world origin (green)
  • The root bone position (red)
  • The bounds rectangle and its center (blue)
  • The draggable area (semi-transparent red), if the web component is set as draggable.

In this example, the root is shifted to avoid overlapping with the origin.

html
<spine-skeleton
   style="width: 200px; height: 200px;"
   identifier="sack-debug"
   atlas="/files/spine-widget/assets/sack-pma.atlas"
   skeleton="/files/spine-widget/assets/sack-pro.skel"
   animation="cape-follow-example"
   drag
   offscreen="pose"
   debug
></spine-skeleton>
js
spine.getSkeleton("sack-debug").whenReady
   .then(({ skeleton }) => skeleton.getRootBone().x += 50);

Page

When using multiple atlas pages (for example, one page per skin) and only a subset of pages need to be displayed, the pages attribute can be used to specify which atlas pages to load. Provide a comma-separated list of the desired page indices.

pages="0,6"
pages="0,4"
pages="0,1"

To load textures programmatically, set the pages attribute to an empty value: pages="". This loads the skeleton and atlas data without loading any textures, allowing textures to be loaded manually at a later time.

html
<spine-skeleton
   identifier="dragon"
   style="flex: 0.8; height: 100%;"
   atlas="/files/spine-widget/assets/dragon-pma.atlas"
   skeleton="/files/spine-widget/assets/dragon-ess.skel"
   animation="flying"
   pages=""
></spine-skeleton>

<input type="button" value="Load page 0" onclick="loadPageDragon(0)" />
<input type="
button" value="Load page 1" onclick="loadPageDragon(1)" />
<input type="
button" value="Load page 2" onclick="loadPageDragon(2)" />
<input type="
button" value="Load page 3" onclick="loadPageDragon(3)" />
<input type="
button" value="Load page 4" onclick="loadPageDragon(4)" />
js
async function loadPageDragon(pageIndex) {
   const dragon = await spine.getSkeleton("dragon").whenReady;
   if (!dragon.pages.includes(pageIndex)) {
      dragon.pages.push(pageIndex);
      dragon.loadTexturesInPagesAttribute();
   }
}

Follow slot

An HTMLElement can be made to follow a slot. This is useful for integrating dynamic content such as text into animations.

Call the followSlot function with these parameters:

  • The Slot instance or slot name to follow

  • The HTMLElement that will follow the slot

  • An "options" object with these properties:

    • followOpacity: links the element's opacity to the slot's alpha
    • followScale: links the element's scale to the slot's scale
    • followRotation: links the element's rotation to the slot's rotation
    • followVisibility: shows or hides the element based on whether the slot has an attachment visible
    • hideAttachment: hides the slot's attachment, as if the element visually replaces it

html
<spine-skeleton
   style="width: 200px; height: 200px;"
   identifier="potty"
   atlas="/files/spine-widget/assets/cloud-pot-pma.atlas"
   skeleton="/files/spine-widget/assets/cloud-pot.skel"
   animation="playing-in-the-rain"
></spine-skeleton>

<div id="rain/rain-color" style="font-size: 50px; display: none;">A</div>
<div id="rain/rain-white" style="font-size: 50px; display: none;">B</div>
<div id="rain/rain-blue" style="font-size: 50px; display: none;">C</div>
<div id="rain/rain-green" style="font-size: 50px; display: none;">D</div>
js
const wc = await spine.getSkeleton("potty").whenReady;
const options = { followVisibility: false, hideAttachment: true };
wc.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), options);
wc.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), options);
wc.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), options);
wc.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), options);

followSlot works even with other spine web components! It works even when it is dragged!

html
<spine-skeleton
   style="width: 200px; height: 200px;"
   identifier="potty2"
   atlas="/files/spine-widget/assets/cloud-pot-pma.atlas"
   skeleton="/files/spine-widget/assets/cloud-pot.skel"
   animation="rain"
   drag
   offscreen="pose"
></spine-skeleton>

<spine-skeleton identifier="potty2-1" atlas="/files/spine-widget/assets/raptor-pma.atlas" skeleton="/files/spine-widget/assets/raptor-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-2" atlas="/files/spine-widget/assets/spineboy-pma.atlas" skeleton="/files/spine-widget/assets/spineboy-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-3" atlas="/files/spine-widget/assets/celestial-circus-pma.atlas" skeleton="/files/spine-widget/assets/celestial-circus-pro.skel" animation="wings-and-feet" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-4" atlas="/files/spine-widget/assets/goblins-pma.atlas" skeleton="/files/spine-widget/assets/goblins-pro.skel" skin="goblingirl" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
js
const wc = await spine.getSkeleton("potty2").whenReady;
const options = { followVisibility: false, hideAttachment: true };
wc.followSlot("rain/rain-color", spine.getSkeleton("potty2-1"), options);
wc.followSlot("rain/rain-white", spine.getSkeleton("potty2-2"), options);
wc.followSlot("rain/rain-blue", spine.getSkeleton("potty2-3"), options);
wc.followSlot("rain/rain-green", spine.getSkeleton("potty2-4"), options);

Advanced usage

Programmatic creation

A <spine-skeleton> element can be created programmatically using the createSkeleton function. This function accepts an object where each property corresponds to a camelCase version of the web component's attributes.

html
<div style="display: flex; flex-wrap: wrap; justify-content: space-evenly; align-items: center; width: 100%;">
   <script>
      ["soeren", "sinisa", "luke"].forEach(skin => {
         ["emotes/wave", "movement/trot-left", "emotes/idea", "emotes/hooray"].forEach(animation => {
         const wc = spine.createSkeleton({
            atlasPath: "/files/spine-widget/assets/chibi-stickers-pma.atlas",
            skeletonPath: "/files/spine-widget/assets/chibi-stickers.skel",
            animation,
            skin,
            pages: [0, 3, 7, 8],
         });
         wc.style.width = "25%";
         wc.style.height = "100px";
         document.currentScript.parentElement.appendChild(wc);
         })
      })
   </script>
</div>

Alternatively, the web component can be directly appended to the DOM as HTML using standard DOM manipulation methods.

html
<div style="display: flex; flex-wrap: wrap; justify-content: space-evenly; align-items: center; width: 100%;">
   <script>
      ["harri", "misaki", "spineboy"].forEach(skin => {
         ["emotes/wave", "movement/trot-left", "emotes/idea", "emotes/hooray"].forEach(animation => {
         document.currentScript.parentElement.insertAdjacentHTML('beforeend', `<spine-skeleton
            style="width: 25%; height: 100px;"
            atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
            skeleton="/files/spine-widget/assets/chibi-stickers.skel"
            animation="${animation}"
            skin="${skin}"
            pages="0,2,5,9"
         ></spine-skeleton>
         `);
         });
      });
   </script>
</div>

By default, assets load immediately.

By default, assets are loaded immediately. To delay asset loading, set manualStart: "false". After creation, append the element to the DOM using the asynchronous appendTo method. At the appropriate time, call start() on the element to begin loading. Any interaction with the Skeleton or AnimationState must be deferred until whenReady resolves.

Dispose

Removing a web component from the DOM does not automatically dispose of it, as it may be intended for reuse elsewhere. To explicitly dispose it, call its dispose() method. This operation is safe and will not release resources still in use by other web components.

To dispose of all spine-webcomponents resources, call dispose() on the overlay instance.

The dispose.html shows how to use the dispose function.

Manual overlays

When a <spine-skeleton> is added to the page, a <spine-overlay> is automatically appended to contain the WebGL canvas where skeletons are rendered. This overlay spans the entire browser viewport.

Alternatively, a <spine-overlay> can be manually appended to a specific HTML element. In this case, it inherits the size of its parent. To render a <spine-skeleton> into a manually defined overlay, assign the same overlay-id to both the skeleton and the overlay.

Regardless of where the overlay is placed within an HTML element, it will always reposition itself as the last child. This ensures the overlay remains on top of other elements. To prevent unnecessary DOM detachments and reattachments, it is recommended to place the overlay as the last element inside the desired container.

Manual overlay creation is useful in these scenarios:

  1. Scrollable containers
    • The skeleton may overflow the container until the container element is fully visible.
    • The skeleton may scroll with some lag, especially on displays with low refresh rates.

  2. Fixed/sticky containers
    • The skeleton may scroll in a jerky or uneven manner.

  3. Custom overlay placement
    • Useful when a smaller overlay is needed, or when the overlay should not be appended directly to the <body>, but to a specific container node.

These issues can be observed in the example below. The first scrollable list uses the default overlay, while the second uses a dedicated overlay inside the scrollable <div>. Clicking the button demonstrates the jerky scrolling behavior by setting the position of the container <div> to fixed.

html
<div style="display: flex; flex-direction: column;">
   <button id="popup-overlay-button-open">Set fixed position</button>
   <div style="height: 250px; display: flex;">
      <div id="fixed" style="display: flex;">
         <div style="overflow-y: auto; width: 150px; height: 250px; border: 1px solid black; padding: 1px; background: white;">
         <script>
            for (let i = 0; i < 6; i++)
               document.currentScript.parentElement.insertAdjacentHTML('beforeend', `
               <spine-skeleton style="height:80px; width: 120px; border: 1px solid black;"
                  atlas="/files/spine-widget/assets/spineboy-pma.atlas"
                  skeleton="/files/spine-widget/assets/spineboy-pro.skel"
                  animation="walk"
               ></spine-skeleton>`);
         </script>
         </div>
         <div style="overflow-y: auto; width: 150px; height: 250px; border: 1px solid black; padding: 1px; background: white;">
         <spine-overlay overlay-id="scroll"></spine-overlay>
         <script>
            for (let i = 0; i < 6; i++)
               document.currentScript.parentElement.insertAdjacentHTML('beforeend', `
               <spine-skeleton style="height:80px; width: 120px; border: 1px solid black;"
                  overlay-id="scroll"
                  atlas="/files/spine-widget/assets/spineboy-pma.atlas"
                  skeleton="/files/spine-widget/assets/spineboy-pro.skel"
                  animation="walk"
               ></spine-skeleton>`);
         </script>
         </div>
      </div>
   </div>
</div>
js
let positionFixed = false;
const openPopupButton = document.getElementById('popup-overlay-button-open');
const popupOverlay = document.getElementById('fixed');
openPopupButton.addEventListener('click', function() {
   if (positionFixed) {
      popupOverlay.style.position = "";
      popupOverlay.style.top = "";
      popupOverlay.style.background = "";
      openPopupButton.innerText = "Set fixed position";
   } else {
      popupOverlay.style.position = 'fixed';
      popupOverlay.style.top = 'calc(50% - 125px)';
      popupOverlay.style.background = 'white';
      openPopupButton.innerText = "Unset fixed position";
   }
   positionFixed = !positionFixed;
});

Notice that each overlay creates its own WebGL contex, which counts against the maximum number of allowed WebGL contexts.