OTTODesign System

Code

Skeleton

Storybook group: Components · Sidebar path: Components/Skeleton · Extracted 28.09.2026

Version Tag Status API
v1 <oc-skeleton-v1> Stable, allowed for generation SkeletonV1

Overview (v1)

Source: ./src/components/skeleton/v1/Overview.mdx

Skeleton

The skeleton component provides a placeholder for content while it's loading. It has attributes for setting the shape, width, height, border radius, and visibility state. The skeleton can be a rectangle or a circle, and its dimensions and border radius can be customized. The visibility state can be toggled, with changes applied using a fade-in/out transition.

Default variation

Story Default:

<oc-skeleton-v1></oc-skeleton-v1>

Configuration

The skeleton component offers a variety of styling variants such as circle and rectangle. This component is configurable, allowing you to tailor its features and appearance to your specific needs. To explore all the available options and adjust the component, use the component configurator and see the changes affect the component in real-time.

Usage guidelines

Before integrating the skeleton component into your project, make sure you have correctly installed the OTTO components package. Look through the variations page for examples of possible component variations. Here, you can discover both common and specific variations that address different use cases.

Accessibility

The skeleton component comes with a set of built-in accessibility features to ensure a seamless experience for all users.

Configuration (v1)

Source: ./src/components/skeleton/v1/Configuration.mdx

Skeleton configuration

Configure the skeleton component with the controls below and see the changes live in the preview canvas. Click the Show code button within the preview canvas to see the source code for the current component configuration.

Story Default:

<oc-skeleton-v1></oc-skeleton-v1>

Interactive configurator (Storybook controls); every option is listed in the API section of this file.

API v1

Source: ./src/components/skeleton/v1/SkeletonV1.API.g.mdx

Skeleton v1 API

API: <oc-skeleton-v1> (SkeletonV1)

The {Latest.default.name.toLowerCase()} component provides a placeholder for content while it's loading. It has attributes for setting the shape, width, height, border radius, and visibility state. The skeleton can be a rectangle or a circle, and its dimensions and border radius can be customized. The visibility state can be toggled, with changes applied using a fade-in/out transition.

Attributes / properties
Attribute Type Default Required Description
variant "rectangle" | "circle" "rectangle" no Sets the main shape of the skeleton.
hidden boolean false no Toggles the state of the skeleton between visible and hidden. Set to true to hide the skeleton. State changes will be applied with a fade-in/out transition.
width string undefined no Sets the width of the skeleton.
height string undefined no Sets the height of the skeleton. This attribute only applies to variant="rectangle".
border-radius string "8px" no Sets the border radius of the rectangular skeleton. This attribute only applies to variant="rectangle".
Events
Event Detail type Description
oc-property-change OcSkeletonV1Events["oc-property-change"] Whenever a property value changes, this event triggers. Use this event to track all property changes within the component.

Refer to the Events documentation for more information.
oc-mount { component: string; } Fired when the component is mounted to the DOM. The event is fired when the onMount hook of the component is called by the runtime.
oc-unmount { component: string; } Fired when the component is unmounted from the DOM. The event is fired when the function returned by the onMount hook of the component is called by the runtime.

Variations (v1)

Source: ./src/components/skeleton/v1/Variations.mdx

Variations

Listed below are the most common variations of the skeleton component as well as specific component variations for different use cases.

You can explore all available options using the component configurator, adjust the component to your needs, and see the changes live in a preview canvas.

Default

Story: components-skeleton-variations--default · tags: components

The default configuration uses variant=rectangle and an 8px border radius.

<oc-skeleton-v1></oc-skeleton-v1>

Rectangular skeleton without border radius

Story: components-skeleton-variations--rectangle-without-border-radius · tags: components

The rectangle variant without a border radius.

Args: variant=rectangle, border-radius=0px, width=400px, height=100px

<oc-skeleton-v1 variant="rectangle" border-radius="0px" width="400px" height="100px"></oc-skeleton-v1>

A circular skeleton

Story: components-skeleton-variations--circle · tags: components

The circle variant with a width of 200px.

Args: variant=circle, width=200px

<oc-skeleton-v1 variant="circle" width="200px"></oc-skeleton-v1>

Demo: collection of skeletons

Story: components-skeleton-variations--demo-skeleton-collection · tags: components

A collection of skeletons with a custom layout and a button to toggle their visibility using the hidden attribute.

<div class="collection">
  <oc-skeleton-v1
    style="display: block; margin-bottom: 12px;"
    width="100%"
    height="103px"
    border-radius="0px"
  ></oc-skeleton-v1>

  <div style="display: flex; justify-content: space-between; margin-bottom: 11px;">
    <oc-skeleton-v1 border-radius="8px" width="114px" height="13px"></oc-skeleton-v1>
    <oc-skeleton-v1 border-radius="8px" width="93px" height="13px"></oc-skeleton-v1>
  </div>

  <div style="display: flex; justify-content: space-between; margin-bottom: 24px">
    <oc-skeleton-v1 border-radius="8px" width="64px" height="13px"></oc-skeleton-v1>
    <oc-skeleton-v1 border-radius="8px" width="143px" height="13px"></oc-skeleton-v1>
  </div>

  <div style="display: flex; gap: 4px; margin-bottom: 17px">
    <oc-skeleton-v1
      border-radius="16px"
      width="81px"
      height="48px"
      style="flex-shrink: 0;"
    ></oc-skeleton-v1>
    <oc-skeleton-v1
      border-radius="16px"
      width="81px"
      height="48px"
      style="flex-shrink: 0;"
    ></oc-skeleton-v1>
    <oc-skeleton-v1
      border-radius="16px"
      width="81px"
      height="48px"
      style="flex-shrink: 0;"
    ></oc-skeleton-v1>
  </div>

  <div style="align-items: center; display: flex; gap: 17px">
    <oc-skeleton-v1
      style="display: inline-block; flex-shrink: 0;"
      variant="circle"
      width="64px"
    ></oc-skeleton-v1>

    <div style="display: flex; flex-direction: column; gap: 8px; width: 100%;">
      <oc-skeleton-v1 width="100%" height="8px"></oc-skeleton-v1>
      <oc-skeleton-v1 width="100%" height="8px"></oc-skeleton-v1>
    </div>
  </div>
</div>

<p>
  <oc-button-v1 variant="secondary" size="50" type="button" id="toggle-button"
    >Toggle Skeleton
  </oc-button-v1>
</p>

<script>
  (() => {
    const skeletons = document.querySelectorAll(".collection oc-skeleton-v1");
    const button = document.getElementById("toggle-button");
    let hidden = false;

    button.addEventListener("click", () => {
      hidden = !hidden;
      for (let skeleton of skeletons) {
        skeleton.hidden = hidden;
      }
    });
  })();
</script>

<style>
  .collection {
    width: 220px;
    height: 310px;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
    margin: 46px;
  }
</style>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: collection of skeletons",
  parameters: {
    controls: {
      disabled: true
    },
    chromatic: {
      hideInChromatic: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <div class="collection">
        <oc-skeleton-v1
          style="display: block; margin-bottom: 12px;"
          width="100%"
          height="103px"
          border-radius="0px"
        ></oc-skeleton-v1>

        <div style="display: flex; justify-content: space-between; margin-bottom: 11px;">
          <oc-skeleton-v1 border-radius="8px" width="114px" height="13px"></oc-skeleton-v1>
          <oc-skeleton-v1 border-radius="8px" width="93px" height="13px"></oc-skeleton-v1>
        </div>

        <div style="display: flex; justify-content: space-between; margin-bottom: 24px">
          <oc-skeleton-v1 border-radius="8px" width="64px" height="13px"></oc-skeleton-v1>
          <oc-skeleton-v1 border-radius="8px" width="143px" height="13px"></oc-skeleton-v1>
        </div>

        <div style="display: flex; gap: 4px; margin-bottom: 17px">
          <oc-skeleton-v1
            border-radius="16px"
            width="81px"
            height="48px"
            style="flex-shrink: 0;"
          ></oc-skeleton-v1>
          <oc-skeleton-v1
            border-radius="16px"
            width="81px"
            height="48px"
            style="flex-shrink: 0;"
          ></oc-skeleton-v1>
          <oc-skeleton-v1
            border-radius="16px"
            width="81px"
            height="48px"
            style="flex-shrink: 0;"
          ></oc-skeleton-v1>
        </div>

        <div style="align-items: center; display: flex; gap: 17px">
          <oc-skeleton-v1
            style="display: inline-block; flex-shrink: 0;"
            variant="circle"
            width="64px"
          ></oc-skeleton-v1>

          <div style="display: flex; flex-direction: column; gap: 8px; width: 100%;">
            <oc-skeleton-v1 width="100%" height="8px"></oc-skeleton-v1>
            <oc-skeleton-v1 width="100%" height="8px"></oc-skeleton-v1>
          </div>
        </div>
      </div>

      <p>
        <oc-button-v1 variant="secondary" size="50" type="button" id="toggle-button"
          >Toggle Skeleton
        </oc-button-v1>
      </p>

      <script>
        (() => {
          const skeletons = document.querySelectorAll(".collection oc-skeleton-v1");
          const button = document.getElementById("toggle-button");
          let hidden = false;

          button.addEventListener("click", () => {
            hidden = !hidden;
            for (let skeleton of skeletons) {
              skeleton.hidden = hidden;
            }
          });
        })();
      </script>

      <style>
        .collection {
          width: 220px;
          height: 310px;
          overflow-x: hidden;
          display: flex;
          flex-direction: column;
          margin: 46px;
        }
      </style>
    `;
  }
}

Demo: loading image

Story: components-skeleton-variations--demo-loading-image · tags: components

This demo showcases skeletons that are replaced by an image after it loads.

<div class="layout">
  <div class="container">
    <oc-skeleton-v1
      width="100%"
      height="100%"
      border-radius="0px"
      class="skeleton"
      hidden
    ></oc-skeleton-v1>
  </div>

  <div class="container">
    <oc-skeleton-v1 width="100%" height="100%" class="skeleton" hidden></oc-skeleton-v1>
  </div>

  <div class="container" style="width: 150px; height: 150px;">
    <oc-skeleton-v1
      width="150px"
      height="150px"
      variant="circle"
      class="skeleton"
      hidden
    ></oc-skeleton-v1>
  </div>
</div>

<br />

<p>
  <oc-button-v1 variant="secondary" size="50" type="button" id="loading-button"
    >Load image
  </oc-button-v1>
</p>

<script>
  (() => {
    const button = document.getElementById("loading-button");
    const container = document.querySelectorAll(".container");

    function load() {
      for (let element of container) {
        const prevImage = element.querySelector("img");
        const skeleton = element.querySelector("oc-skeleton-v1");
        skeleton.hidden = false;
        if (prevImage) {
          prevImage.parentNode.removeChild(prevImage);
        }
        const image = document.createElement("img");
        image.onload = () => {
          skeleton.hidden = true;
        };
        element.append(image);

        // simulate slower image loading
        setTimeout(() => {
          image.setAttribute("src", "${placeholderImage}");
        }, 2000);
      }
    }

    button.addEventListener("click", () => {
      load();
    });
  })();
</script>

<style>
  .layout {
    display: flex;
    flex-flow: row wrap;
    gap: 10px;
  }
  .container {
    position: relative;
    width: 200px;
    height: 150px;
    overflow: hidden;
  }

  .container img {
    object-fit: contain;
  }

  .skeleton {
    position: absolute;
  }
</style>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: loading image",
  parameters: {
    controls: {
      disabled: true
    },
    chromatic: {
      hideInChromatic: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <div class="layout">
        <div class="container">
          <oc-skeleton-v1
            width="100%"
            height="100%"
            border-radius="0px"
            class="skeleton"
            hidden
          ></oc-skeleton-v1>
        </div>

        <div class="container">
          <oc-skeleton-v1 width="100%" height="100%" class="skeleton" hidden></oc-skeleton-v1>
        </div>

        <div class="container" style="width: 150px; height: 150px;">
          <oc-skeleton-v1
            width="150px"
            height="150px"
            variant="circle"
            class="skeleton"
            hidden
          ></oc-skeleton-v1>
        </div>
      </div>

      <br />

      <p>
        <oc-button-v1 variant="secondary" size="50" type="button" id="loading-button"
          >Load image
        </oc-button-v1>
      </p>

      <script>
        (() => {
          const button = document.getElementById("loading-button");
          const container = document.querySelectorAll(".container");

          function load() {
            for (let element of container) {
              const prevImage = element.querySelector("img");
              const skeleton = element.querySelector("oc-skeleton-v1");
              skeleton.hidden = false;
              if (prevImage) {
                prevImage.parentNode.removeChild(prevImage);
              }
              const image = document.createElement("img");
              image.onload = () => {
                skeleton.hidden = true;
              };
              element.append(image);

              // simulate slower image loading
              setTimeout(() => {
                image.setAttribute("src", "${placeholderImage}");
              }, 2000);
            }
          }

          button.addEventListener("click", () => {
            load();
          });
        })();
      </script>

      <style>
        .layout {
          display: flex;
          flex-flow: row wrap;
          gap: 10px;
        }
        .container {
          position: relative;
          width: 200px;
          height: 150px;
          overflow: hidden;
        }

        .container img {
          object-fit: contain;
        }

        .skeleton {
          position: absolute;
        }
      </style>
    `;
  }
}