> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Skeleton - Web

> Skeletons are placeholders for content which hasn't yet loaded.

> [!NOTE]
>
> For development, import `Skeleton` from
> `@servicetitan/anvil2`. For design, find it in the
> [Anvil2 file](https://www.figma.com/design/xaNQsvRKczoxu7pj0M6QT8/Anvil2-v3).

![](https://mintcdn.com/servicetitan/H5FwKyiqhPVZ1UJQ/images/docs/web/components/shared/skeleton-main-image.png?fit=max\&auto=format\&n=H5FwKyiqhPVZ1UJQ\&q=85\&s=a8a097a29aa59772106190189df6a4c7)

## Anatomy

The Skeleton consists of two primary elements that work together to create placeholders for content which hasn't yet loaded.

![skeleton
anatomy](https://mintcdn.com/servicetitan/usDdQzJfkl6jYYLR/images/docs/web/components/skeleton/design/skeleton-anatomy.png?fit=max\&auto=format\&n=usDdQzJfkl6jYYLR\&q=85\&s=a47a1c7aae45f7d2a21918634ec499d2)

1. Fill
2. Container

## Options

The Skeleton supports text and shape configurations to accommodate various content placeholder scenarios.

### Text

#### Headline

Use the headline option to indicate loading of a title.

skeleton-text-headline

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-text-headline?file=src/App.tsx)

```tsx lines theme={null}
import { Skeleton } from "@servicetitan/anvil2";

function App() {
  return <Skeleton.Text variant="headline" style={{ width: "300px" }} />;
}

export default App;
```

#### Body Text

Use the body text option to indicate loading of a sentence, string of text, or a paragraph.

skeleton-text-body

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-text-body?file=src/App.tsx)

```tsx lines theme={null}
import { Skeleton, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex direction="column" gap="5" style={{ width: "300px" }}>
      <Skeleton.Text />
      <Skeleton.Text rows={5} />
    </Flex>
  );
}

export default App;
```

### Shape

#### Rectangle

Use the rectangle option to indicate loading of card, button, image, or sectional content.

skeleton-rectangle

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-rectangle?file=src/App.tsx)

```tsx lines theme={null}
import { Skeleton, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex direction="column" gap="5" style={{ width: "300px" }}>
      <Skeleton.Rectangle />
      <Skeleton.Rectangle height="4rem" />
      <Skeleton.Rectangle height="100px" />
    </Flex>
  );
}

export default App;
```

#### Circle

Use the circle option to indicate loading of an avatar.

skeleton-circle

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-circle?file=src/App.tsx)

```tsx lines theme={null}
import { Skeleton, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex direction="row" gap="5">
      <Skeleton.Circle />
      <Skeleton.Circle diameter="50px" />
      <Skeleton.Circle diameter="4rem" />
    </Flex>
  );
}

export default App;
```

#### Pill

Use the pill option to indicate loading of a chip.

skeleton-pill

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-pill?file=src/App.tsx)

```tsx lines theme={null}
import { Skeleton, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex direction="column" gap="5">
      <Skeleton.Pill />
      <Skeleton.Pill width="50px" />
      <Skeleton.Pill width="6rem" />
    </Flex>
  );
}

export default App;
```

### AI

Use the AI option to indicate loading of AI-generated content. All Skeleton variants support an AI style that replaces the default gray gradient with blue gradient colors.

skeleton-ai

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-ai?file=src/App.tsx)

```tsx lines theme={null}
import { Skeleton, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex direction="column" gap="5" style={{ width: "300px" }}>
      <Flex direction="row" gap="3" alignItems="center">
        <Skeleton.Circle ai diameter="2.5rem" />
        <Flex direction="column" gap="2" grow={1}>
          <Skeleton.Text ai variant="headline" />
          <Skeleton.Pill ai width="4rem" />
        </Flex>
      </Flex>
      <Skeleton.Text ai rows={3} />
      <Skeleton.Rectangle ai height="6rem" />
    </Flex>
  );
}

export default App;
```

## Usage Guidelines

| Standard | Requirement |
| - | - |
| DoKnown approximate UI shape | When the implementation knows the approximate UI that is to be loaded |
| DoInitial page load | When loading page initially |
| DoPagination, filtering, or search loads | When loading content through pagination, filtering, or searching |
| DoLoading dynamic content | When loading dynamic content initially. Dynamic content requires an external service to provide it, such as Combobox items, a table, or a card grid. |
| DoMatch the variant to the content shape | Use Skeleton Text (headline variant for a title, body variant for a sentence, string of text, or paragraph), Skeleton Rectangle for a card, button, image, or section, Skeleton Circle for an avatar, and Skeleton Pill for a chip; use the AI option on any variant for AI-generated content. |
| Don'tSub-second loads | Do not show Skeleton or any loading state if loading takes less than 1 second. Content loading in less than 1 second [keeps the user's flow of thought seamless](https://www.nngroup.com/articles/response-times-3-important-limits/). |
| Don'tLoading additional content | Use Spinner for loading additional content, for example in infinite scrolling experiences |

### How to Use

Skeletonized UI should be an approximation of the UI that is loading and not meant to recreate every element that will load.

#### Scope of skeleton

Apply Skeleton to everything in the same scope. In this context, scope refers to component that is awaiting for the data to be fetched.

* Full Skeleton
  * Fetching data to render grid of cards → everything inside the Card is skeletonized
  * Fetching table data → table is skeletonized everything inside the Table is skeletonized
* Partial Skeleton
  * Card as a section containing title and content that requires fetch
    * Title is not part of the fetch scope, so this is static and not to be skeletonized
    * Example of fetch content: Listbox with dynamic options, data visualization, score card (label + data number)
    * Note that if there is higher level fetch that affects other sibling, this Card can also be fully skeletonized. Example: Jira ticket type of page level loading

#### Repeated content

For **repeated** content, use skeleton to fill up **3/4** of the available space. For example, even if a grid of Cards are expected to overflow, only fill 3/4 of the available space with the skeleton.

#### Non-repeated content

For **non-repeated** content, keep it above the fold as much as possible

* This is to reduce unnecessary scrolling of the page while contents are still loading in
* Approximation of the UI can be minified to keep things above the fold
  * Example: Even if we expect a long blog post, it can be reduced to a two-paragraph skeleton to keep it above the fold.

#### Example

skeleton-playground

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-playground?file=src/App.tsx)

```tsx lines theme={null}
import {
  Skeleton,
  Button,
  Flex,
  Card,
  Avatar,
  Text,
} from "@servicetitan/anvil2";
import { useState } from "react";

function App() {
  const [loading, setLoading] = useState(true);

  return (
    <Flex direction="column" gap="4">
      <Card
        flexGrow="1"
        flexDirection="column"
        gap="2"
        style={{ width: "200px" }}
      >
        {loading ? (
          <>
            <Skeleton.Circle diameter="3rem" />
            <Skeleton.Text variant="headline" />
            <Skeleton.Text rows={2} />
          </>
        ) : (
          <>
            <Avatar name="Ben Ho" size="large" />
            <Text variant="headline" el="h6" size="small">
              Ben Ho
            </Text>
            <Text>Lorem ipsum dolor sit amet.</Text>
          </>
        )}
      </Card>
      <Button
        style={{ alignSelf: "start" }}
        onClick={() => setLoading(!loading)}
      >
        Toggle loading
      </Button>
    </Flex>
  );
}

export default App;
```

## Content

Content within the Skeleton should approximate the UI that is loading without recreating every element.

## Keyboard Interaction

Skeletons are not focusable and do not support keyboard interaction.

## Code

**Implementation**

[skeleton-playground](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-playground?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

## Common Examples

### AI Variant

Use the `ai` prop on any `Skeleton` component to apply blue gradient colors, signaling that AI-generated content is loading.

skeleton-ai

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/skeleton-ai?file=src/App.tsx)

```tsx lines expandable theme={null}
import { Skeleton, Flex } from "@servicetitan/anvil2";

function App() {
  return (
    <Flex direction="column" gap="5" style={{ width: "300px" }}>
      <Flex direction="row" gap="3" alignItems="center">
        <Skeleton.Circle ai diameter="2.5rem" />
        <Flex direction="column" gap="2" grow={1}>
          <Skeleton.Text ai variant="headline" />
          <Skeleton.Pill ai width="4rem" />
        </Flex>
      </Flex>
      <Skeleton.Text ai rows={3} />
      <Skeleton.Rectangle ai height="6rem" />
    </Flex>
  );
}

export default App;
```

### With MobX

This example shows how to use Skeleton with MobX, which is what ST app uses for data store. Refer to [How to use](https://anvil.servicetitan.com/docs/components/skeleton/design#how-to-use) guide for the scope of Skeleton.

```tsx theme={null}
import { observer } from "mobx-react-lite";
import { useState, useEffect } from "react";

const MobXExample = () => observer(() => {
  const { isLoading, data } = useStore(); // Assuming you have a MobX store

  return (
    {
      isLoading ? (
        <Card flexGrow="1" flexDirection="column" gap="2">
          <Skeleton.Circle diameter="3rem" />
          <Skeleton.Text variant="headline" />
          <Skeleton.Text rows="3"/>
        </Card>
      ) : (
        <Card flexGrow="1" flexDirection="column" gap="2">
          <Avatar name={data.name} size="large" />
          <Text variant="headline" el="p" size="small">{data.name}</Text>
          <Text>{data.description}</Text>
        </Card>
      )
    }
  )
})
```

## Best Practices

Do not use [`<Suspense>`](https://react.dev/reference/react/Suspense) as it requires the fetch to be using [`use()` hook](https://react.dev/reference/react/use).

**Skeleton.Circle Props**

```tsx theme={null}
<Skeleton.Circle diameter="3rem" />
```

## `Skeleton.Circle` Props

In addition to the props listed below, the `Skeleton.Circle` component can accept any valid HTML `div` props.

**ai** `boolean`

Enables the AI visual style with blue gradient colors.

**diameter** `string` (default: \$size-8)

Must be a valid CSS value.

**Skeleton.Pill Props**

```tsx theme={null}
<Skeleton.Pill width="200px" />
```

## `Skeleton.Pill` Props

In addition to the props listed below, the `Skeleton.Pill` component can accept any valid HTML `div` props.

**ai** `boolean`

Enables the AI visual style with blue gradient colors.

**width** `string`

Must be a valid CSS value.

**Skeleton.Rectangle Props**

```tsx theme={null}
<Skeleton.Rectangle height="100px" />
```

## `Skeleton.Rectangle` Props

In addition to the props listed below, the `Skeleton.Rectangle` component can accept any valid HTML `div` props.

**ai** `boolean`

Enables the AI visual style with blue gradient colors.

**height** `string` (default: \$size-14)

Must be a valid CSS value.

**Skeleton.Text Props**

```tsx theme={null}
<Skeleton.Text variant="body" rows={1} />
```

## `Skeleton.Text` Props

In addition to the props listed below, the `Skeleton.Text` component can accept any valid HTML `div` props.

**ai** `boolean`

Enables the AI visual style with blue gradient colors.

**rows** `number` (default: 1)

Number of text lines to display. Only works with `variant="body"`.

**variant** `"body" | "headline"` (default: body)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.