HextaUI

Skeleton

150ms待ってから表示され、包んだコンテンツと同じサイズになり、何も動かさずにコンテンツをフェードインさせるプレースホルダーです。

import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonDemo() {
  return (
    <div className="flex w-full max-w-sm items-center gap-4">
      <Skeleton className="size-12 rounded-full" />
      <div className="flex flex-1 flex-col gap-2">
        <Skeleton className="h-4 w-3/5" />
        <Skeleton className="h-4 w-4/5" />
      </div>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/skeleton.json

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

shimmer と pulse は、テーマにある2つのアニメーションを使います。グローバル CSS ファイルに一度だけ追加してください。

@import "tw-animate-css";

@theme {
  --animate-shimmer: shimmer 1.6s ease-in-out infinite;
  --animate-skeleton-pulse: skeleton-pulse 2s ease-in-out infinite;

  @keyframes shimmer {
    from {
      translate: calc(-100% * var(--skeleton-dir, 1)) 0;
    }
    to {
      translate: calc(100% * var(--skeleton-dir, 1)) 0;
    }
  }

  @keyframes skeleton-pulse {
    0%,
    100% {
      opacity: 0;
    }
    50% {
      opacity: 0.5;
    }
  }
}

スケルトンは、サイズ指定したプレースホルダーとして単独で使うか、loading を渡して実際のコンテンツを囲み、プレースホルダーにそのまったく同じサイズを取らせます。

import { Skeleton, SkeletonText } from "@/components/ui/skeleton"
<Skeleton className="h-4 w-32" />

<Skeleton loading={isLoading}>
  <h3>{user.name}</h3>
</Skeleton>

形

スケルトン単体は空のブロックです。置き換える対象に合わせて、クラスでサイズと角丸を指定します。

import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonShapes() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <div className="flex items-center gap-4">
        <Skeleton className="size-12 rounded-full" />
        <div className="flex flex-1 flex-col gap-2">
          <Skeleton className="h-4 w-2/5" />
          <Skeleton className="h-4 w-3/5" />
        </div>
        <Skeleton className="h-8 w-20" />
      </div>
      <Skeleton className="aspect-video w-full rounded-xl" />
    </div>
  )
}

アニメーション

animation は、掃引する shimmer、柔らかい pulse、none から選びます。

import { Skeleton, type SkeletonAnimation } from "@/components/ui/skeleton"

const animations: SkeletonAnimation[] = ["shimmer", "pulse", "none"]

export function SkeletonAnimations() {
  return (
    <div className="grid w-full max-w-md grid-cols-3 gap-3">
      {animations.map((animation) => (
        <div key={animation} className="flex flex-col gap-2">
          <Skeleton animation={animation} className="h-16 w-full" />
          <span className="text-xs text-muted-foreground">{animation}</span>
        </div>
      ))}
    </div>
  )
}

テキスト

<SkeletonText /> は1行につき1本のバーを描画し、親のフォントサイズと行の高さに従うため、置き換えるテキストと同じ空間を埋めます。最後の行は短くなります。

import { SkeletonText } from "@/components/ui/skeleton"

export function SkeletonTextDemo() {
  return (
    <div className="grid w-full max-w-md grid-cols-3 gap-6">
      <div className="text-sm">
        <SkeletonText lines={3} />
      </div>
      <div className="text-lg">
        <SkeletonText lines={3} />
      </div>
      <div className="text-sm leading-8">
        <SkeletonText lines={3} />
      </div>
    </div>
  )
}

実際のコンテンツを囲む

loading を指定すると、スケルトンは実際のコンテンツを下に不可視で描画するため、正確なサイズを取り、データが届いてもレイアウトがずれません。loading が false になると、コンテンツがフェードインします。

"use client"

import * as React from "react"
import { IconUserPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonWrapContent() {
  const [loading, setLoading] = React.useState(true)

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <div className="flex items-start gap-4 rounded-xl border p-4">
        <Skeleton loading={loading} className="rounded-full">
          <img
            src="/preview/landscape.svg"
            alt=""
            className="size-12 rounded-full object-cover"
          />
        </Skeleton>
        <div className="flex min-w-0 flex-1 flex-col gap-1">
          <Skeleton loading={loading}>
            <h3 className="font-semibold">Olivia Martin</h3>
          </Skeleton>
          <Skeleton loading={loading}>
            <p className="text-sm text-muted-foreground">
              Design engineer at Acme. Writes about motion and small details.
            </p>
          </Skeleton>
        </div>
        <Skeleton loading={loading}>
          <Button size="sm" variant="outline">
            <IconUserPlus data-icon="inline-start" />
            Follow
          </Button>
        </Skeleton>
      </div>
      <div className="flex gap-2">
        <Button size="sm" onClick={() => setLoading(false)}>
          Load
        </Button>
        <Button size="sm" variant="ghost" onClick={() => setLoading(true)}>
          Reset
        </Button>
      </div>
    </div>
  )
}

インライン

文の中にスケルトンを置くには render={<span />} を渡します。テキストのベースラインに揃います。

import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonInline() {
  return (
    <p className="text-sm">
      Your balance is{" "}
      <Skeleton loading render={<span />}>
        <strong>$12,480.00</strong>
      </Skeleton>{" "}
      as of today.
    </p>
  )
}

高速な読み込み

スケルトンは最初の150msは見えず、その後フェードインします。それより早く届いたデータでは、プレースホルダーが一瞬だけ表示されることはありません。

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonFastLoad() {
  const [loading, setLoading] = React.useState(true)
  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timer.current), [])

  function run() {
    clearTimeout(timer.current)
    setLoading(true)
    timer.current = setTimeout(() => setLoading(false), 80)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Skeleton loading={loading}>
        <p className="text-sm">
          Loaded in 80ms, so the skeleton never became visible.
        </p>
      </Skeleton>
      <Button size="sm" onClick={run}>
        Run fast load
      </Button>
    </div>
  )
}

多数の行

同時にマウントされたスケルトンはアニメーションも同時に開始するため、長いリストも1つの読み込み領域として見えます。

import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonManyRows() {
  return (
    <div className="flex max-h-64 w-full max-w-sm flex-col gap-3 overflow-y-auto">
      {Array.from({ length: 50 }, (_, index) => (
        <div key={index} className="flex items-center gap-3">
          <Skeleton className="size-8 rounded-full" />
          <Skeleton className="h-3 w-1/2" />
        </div>
      ))}
    </div>
  )
}

右から左

右から左のレイアウトでは、shimmer は右から左に掃引します。

import { Skeleton, SkeletonText } from "@/components/ui/skeleton"

export function SkeletonRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-sm items-center gap-4 text-sm">
      <Skeleton className="size-12 rounded-full" />
      <div className="flex-1">
        <SkeletonText lines={2} />
      </div>
    </div>
  )
}
  • スケルトン単体は装飾で、支援技術から隠されます。<SkeletonText /> も同様です。
  • コンテンツを囲むスケルトンは、読み込み中に aria-busy を設定します。下にあるコンテンツは支援技術から隠され、読み込まれるまでフォーカスできません。
  • スケルトンは何も読み上げません。何を読み込んでいるかを伝える必要がある場合は、表示されるラベルかステータスメッセージを追加してください。
  • 視差効果の軽減が有効な場合、スケルトンは shimmer、pulse、フェードなしですぐに表示されます。

描画する要素のすべての属性を受け付けます。

プロパティ型デフォルト
animation
"shimmer" | "pulse" | "none""shimmer"
loading設定すると、スケルトンは子要素を囲みます。true ならプレースホルダー、false ならコンテンツを表示します。単独のプレースホルダーにするには省略します。
boolean–
render
ReactElement | (props, state) => ReactElement<div>
属性説明
data-slot="skeleton"CSS でスケルトンを指定します。
data-animation使用中のアニメーション。囲んでいたコンテンツが読み込まれると取り除かれます。
data-loadingスケルトンがコンテンツを囲んでいるときは "true" または "false"。
data-slot="skeleton-content"実際のコンテンツを囲みます。読み込み中は不可視で inert です。
--skeleton-dir1、右から左のレイアウトでは -1。shimmer の方向を設定します。

すべての <div> 属性を受け付けます。

プロパティ型デフォルト
lines切り捨てられ、1〜50の範囲に保たれます。
number3
animation
"shimmer" | "pulse" | "none""shimmer"
属性説明
data-slot="skeleton-text"行を入れるコンテナー。

使用しているブロック

Skeleton の上に構築されるブロック。