Primitives
Skeleton
Sharp sweeping placeholder.
● LIVE
"use client";import { Skeleton } from "@/registry/okibi/ui/skeleton";export default function SkeletonDemo() { return ( <div className="flex items-center gap-4"> <Skeleton className="size-12" /> <div className="space-y-2"> <Skeleton className="h-4 w-48" /> <Skeleton className="h-4 w-32" /> </div> </div> );}npx shadcn@latest add https://okibi.cndr.dev/r/skeleton.jsonInstallation
Install the theme first, then add the component:
npx shadcn@latest add https://okibi.cndr.dev/r/skeleton.jsonCopy the source from the Code tab above into components/ui/skeleton.tsx. It uses the cn helper and the okibi theme tokens — install the theme first if you haven't.
Usage
import { Skeleton } from "@/components/ui/skeleton"
<Skeleton className="h-8 w-48" />Examples
Card
Layer several Skeleton blocks into a card silhouette — image, avatar, lines — for a real loading placeholder.
● LIVE
"use client";import { Skeleton } from "@/registry/okibi/ui/skeleton";export default function SkeletonCardDemo() { return ( <div className="flex w-72 flex-col gap-4 border border-field bg-surface-1 p-4"> <Skeleton className="h-32 w-full" /> <div className="flex items-center gap-3"> <Skeleton className="size-10 shrink-0" /> <div className="flex-1 space-y-2"> <Skeleton className="h-4 w-3/4" /> <Skeleton className="h-4 w-1/2" /> </div> </div> <div className="space-y-2"> <Skeleton className="h-3 w-full" /> <Skeleton className="h-3 w-full" /> <Skeleton className="h-3 w-4/5" /> </div> </div> );}Accessible loading
Wrap the placeholders in role="status" + aria-busy with an sr-only Loading label so assistive tech announces the pending state.
● LIVE
Loading…
"use client";import { Skeleton } from "@/registry/okibi/ui/skeleton";export default function SkeletonStatusDemo() { return ( <div role="status" aria-busy="true" className="flex items-center gap-4"> <Skeleton className="size-12" /> <div className="space-y-2"> <Skeleton className="h-4 w-48" /> <Skeleton className="h-4 w-32" /> </div> <span className="sr-only">Loading…</span> </div> );}Accessibility
- A
Skeletonis an unopinionateddivand is silent to assistive tech on its own — it conveys no loading state by itself. - Wrap the loading region in a container with
role='status'andaria-busy='true', and include ansr-only'Loading…' label so the wait is announced. - Mark the placeholder shapes
aria-hiddenso screen readers hear the status label, not a pile of empty boxes. - Visibility comes from the
border-fieldboundary (the dimsurface-2fill alone is ~1.1:1), clearing the WCAG 1.4.11 non-text 3:1 bar in both modes. - The highlight sweep is reduced-motion-gated and rests at the legible bordered fill, which stays readable on its own.
Guidelines
Do
- Size and shape skeletons with Tailwind utilities on
className(h-8 w-48) — thatclassNameis the only knob the component takes. - Mirror the real content's layout so the swap-in doesn't shift the page.
- Announce the wait at the region level with
role='status'+aria-busyrather than per-skeleton.
Don't
- Don't expect a skeleton to announce loading — it's a styled
div; therole='status'wrapper does that. - Don't drop the
border-fieldboundary; without it the dim fill is effectively invisible. - Don't reach for opacity pulsing — the on-brand motion is an opaque highlight sweep that never fades toward the card.