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.
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:
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:
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 |
<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.
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.
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.
.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.
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:
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:
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.
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 ratioscaleDown: scale the skeleton down to ensure that the skeleton fits within the container elementnone: 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.
atlas="/files/spine-widget/assets/raptor-pma.atlas"
skeleton="/files/spine-widget/assets/raptor-pro.skel"
identifier="raptor"
></spine-skeleton>
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.
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.
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.
auto-calculate-bounds set auto-calculate-bounds 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>
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" 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>
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:
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:
trueto loop the animation (falseif omitted) - delay: seconds to wait after the previous animation starts, or
0to wait until the previous animation ends (0if omitted) - mixDuration: seconds to mix from the previous animation to this animation (
default-mixif 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 (
0if 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:
[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:
[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.
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.
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)" />
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:
pointerWorldXandpointerWorldY: the x and y coordinates of the pointer relative to the skeleton origin (Spine world coordinates).worldXandworldY: the x and y coordinates of the skeleton origin relative to the canvas/WebGL context origin (Spine world coordinates).
For spine-overlay:
pointerCanvasXandpointerCanvasY: the x and y coordinates of the pointer relative to the top-left corner of the canvas (screen coordinates).pointerWorldXandpointerWorldY: 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.
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>
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) => voidto 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:
pointerEventCallbacktriggers thejumpanimation onenter, and thewaveanimation onleave.addPointerSlotEventCallbackadds a callback for thehead-baseslot (the face). When the attachment receives adownevent, the normal and dark tint are updated based on the selected colors in the two tint selectors.
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;"/>
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.
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>
.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.
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)" />
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
Slotinstance or slot name to follow -
The
HTMLElementthat will follow the slot -
An "options" object with these properties:
followOpacity: links the element's opacity to the slot's alphafollowScale: links the element's scale to the slot's scalefollowRotation: links the element's rotation to the slot's rotationfollowVisibility: shows or hides the element based on whether the slot has an attachment visiblehideAttachment: hides the slot's attachment, as if the element visually replaces it
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>
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!
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>
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.
<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.
<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:
- 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.
- Fixed/sticky containers
- The skeleton may scroll in a jerky or uneven manner.
- 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.
- Useful when a smaller overlay is needed, or when the overlay should not be appended directly to the
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.
<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>
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.