# Skeleton

> A placeholder that shows the shape of content being loaded. A CSS component with one class and no JavaScript.

Grey shapes that show where the content will appear. They are always hidden from screen readers, so the wait has to be announced somewhere as well. If there is no shape to show, or the wait is short, [Spinner](/components/spinner.md) is simpler.

## How to use it

**Plain HTML**

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/skeleton/skeleton.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css -->
```

```html
<div class="lt-skeleton" data-variant="circle" aria-hidden="true"></div>
<div class="lt-skeleton" data-variant="text" aria-hidden="true"></div>
<div class="lt-skeleton case-last-line" data-variant="text" aria-hidden="true"></div>
<div class="lt-skeleton case-image" aria-hidden="true"></div>
```

```css
/* Your own classes. These two rules are all it takes. */
.case-last-line {
inline-size: 70%;
}


.case-image {
--lt-skeleton-height: var(--lt-spacing-12);
}
```

**TypeScript**

```ts
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/skeleton.css"
```

`lt` is used in two ways, and which one depends on whether you create the element or find it.

**If you create the markup,** spread the attributes in. That is the way in React, Astro and anything else that renders:

```tsx
import { lt } from "@lo-tek/interfaces/react"


<div {...lt.skeleton({ variant: "circle" })} />
```

The import is from `/react` here, since the block is JSX. From there the keys are `className` and `htmlFor`. In Astro and anything else without React, import `@lo-tek/interfaces` and you get `class` and `for`.

**If you have the element in a variable,** set them on it with `setAttributes`. That is the way without JSX, whether you just created the element or found it in a page the server sent:

```ts
import { lt } from "@lo-tek/interfaces"


const line = document.createElement("div")


lt.setAttributes(line, lt.skeleton({ variant: "text" }))
```

| `data-variant` | Used for |
| - | - |
| *(omitted)* | A block, for images and cards |
| `text` | One line of text |
| `circle` | A profile picture |

The block and the text lines fill the width they are given, while `circle` is as wide as it is high. `case-last-line` and `case-image` are your own classes, not LO-TEK’s, and they are in the “Plain HTML” tab above together with the markup.

Set the width of the last line to less than 100 per cent. A paragraph where every line is the same length does not look like text.

## The wait has to be announced somewhere

```html
<div id="content" aria-busy="true">
  <p class="lt-sr-only" role="status">Fetching your applications</p>
  <div class="lt-skeleton" data-variant="text" aria-hidden="true"></div>
  <div class="lt-skeleton" data-variant="text" aria-hidden="true"></div>
</div>
```

A row of empty boxes being read out is worse than silence. That is why `lt.skeleton()` always sets `aria-hidden="true"`, and the message goes in a separate element with `role="status"`.

When the content arrives, remove `aria-busy` and empty the status element.

## Classes you can style

| Class | What it is |
| - | - |
| `.lt-skeleton` | The placeholder. The shape comes from `data-variant`. |

| Variable | Default |
| - | - |
| `--lt-skeleton-height` | `var(--lt-spacing-4)`, and `var(--lt-spacing-10)` in `circle`, where it also gives the width. `text` does not read it: there the height comes from `--lt-font-size-m` |
| `--lt-skeleton-radius` | `var(--lt-spacing-1)`, and `var(--lt-spacing-0-5)` in `text` |

The shimmer is a gradient moving across the surface, not a pulsing transparency, so the contrast against the page surface is the same throughout. If the user has asked for reduced motion, the surface stays still.

See [Customising](/customising.md) for how the variables and the layer work.

## Accessibility

- The shapes should resemble what is coming. A placeholder of three lines where the result is twenty makes the page jumpy when the content lands.
- Wait a little before showing them, as with a loading indicator. A flash of grey boxes is more disruptive than a short pause.
- Do not leave them in place if something fails. The user is then waiting for something that never comes. Replace them with an [Alert](/components/alert.md) saying what went wrong.
