Skip to content
touchcn
Esc
navigateopen⌘Jpreview
On this page

Skeleton

A placeholder shown while content is loading.

A placeholder that stands in for content while it loads. Android sweeps a shimmer gradient across it; iOS pulses its opacity. Pick a shape with variant and size it with utility classes.

Installation

npx touchcn add skeleton

Usage

<tcn-skeleton variant="text" class="w-3/4" />
<tcn-skeleton variant="rect" class="h-24" />
<tcn-skeleton variant="circle" class="size-10" />
import { TcnSkeleton } from '@/components/ui/skeleton';

<TcnSkeleton variant="text" className="w-3/4" />
<TcnSkeleton variant="rect" className="h-24" />
<TcnSkeleton variant="circle" className="size-10" />
<script setup lang="ts">
import { TcnSkeleton } from '@/components/ui/skeleton';
</script>

<template>
  <TcnSkeleton variant="text" class="w-3/4" />
  <TcnSkeleton variant="rect" class="h-24" />
  <TcnSkeleton variant="circle" class="size-10" />
</template>

The element is aria-hidden so it stays out of the accessibility tree, and its animation freezes static under prefers-reduced-motion.

Props

Prop Type Default Description
variant 'text' | 'circle' | 'rect' 'text' Placeholder shape.
Source · Angular
export * from './tcn-skeleton';
import { Component, computed, ElementRef, inject, input } from '@angular/core';
import { cn } from '@touchcn/core';

export type TcnSkeletonVariant = 'text' | 'circle' | 'rect';

const variantClass: Record<TcnSkeletonVariant, string> = {
  text: 'tcn-skeleton-text',
  circle: 'tcn-skeleton-circle',
  rect: 'tcn-skeleton-rect',
};

/**
 * Placeholder shown while content loads. `variant` picks the shape (a text line,
 * a circle for avatars, or a rectangle for media/cards); size it further with
 * utility classes. MD sweeps a shimmer gradient across it, iOS pulses its
 * opacity — both idiomatic, all in `theme.css`. `aria-hidden` keeps it out of
 * the accessibility tree, and `prefers-reduced-motion` freezes it static.
 */
@Component({
  selector: 'tcn-skeleton',
  template: '',
  host: {
    'aria-hidden': 'true',
    '[class]': 'classes()',
  },
})
export class TcnSkeleton {
  private readonly authored = inject(ElementRef<HTMLElement>).nativeElement.getAttribute('class') ?? '';

  readonly variant = input<TcnSkeletonVariant>('text');

  protected readonly classes = computed(() => cn('tcn-skeleton', variantClass[this.variant()], this.authored));
}
Source · React
export * from './tcn-skeleton';
import { forwardRef } from 'react';
import type { ComponentPropsWithoutRef } from 'react';
import { cn } from '@touchcn/core';

export type TcnSkeletonVariant = 'text' | 'circle' | 'rect';

const variantClass: Record<TcnSkeletonVariant, string> = {
  text: 'tcn-skeleton-text',
  circle: 'tcn-skeleton-circle',
  rect: 'tcn-skeleton-rect',
};

export interface TcnSkeletonProps extends ComponentPropsWithoutRef<'span'> {
  variant?: TcnSkeletonVariant;
}

/**
 * Placeholder shown while content loads. `variant` picks the shape (a text line,
 * a circle for avatars, or a rectangle for media/cards); size it further with
 * utility classes. MD sweeps a shimmer gradient across it, iOS pulses its
 * opacity — both idiomatic, all in `theme.css`. `aria-hidden` keeps it out of
 * the accessibility tree, and `prefers-reduced-motion` freezes it static.
 */
export const TcnSkeleton = forwardRef<HTMLSpanElement, TcnSkeletonProps>(function TcnSkeleton(
  { variant = 'text', className, ...props },
  ref,
) {
  return <span ref={ref} aria-hidden="true" className={cn('tcn-skeleton', variantClass[variant], className)} {...props} />;
});
Source · Vue
<script lang="ts">
export type TcnSkeletonVariant = 'text' | 'circle' | 'rect';

const variantClass: Record<TcnSkeletonVariant, string> = {
  text: 'tcn-skeleton-text',
  circle: 'tcn-skeleton-circle',
  rect: 'tcn-skeleton-rect',
};
</script>

<script setup lang="ts">
import { computed } from 'vue';

/**
 * Placeholder shown while content loads. `variant` picks the shape (a text line,
 * a circle for avatars, or a rectangle for media/cards); size it further with
 * utility classes. MD sweeps a shimmer gradient across it, iOS pulses its
 * opacity — both idiomatic, all in `theme.css`. `aria-hidden` keeps it out of
 * the accessibility tree, and `prefers-reduced-motion` freezes it static.
 */
const props = withDefaults(defineProps<{ variant?: TcnSkeletonVariant }>(), { variant: 'text' });

const classes = computed(() => ['tcn-skeleton', variantClass[props.variant]]);
</script>

<template>
  <span aria-hidden="true" :class="classes" />
</template>
export { default as TcnSkeleton } from './TcnSkeleton.vue';
export type { TcnSkeletonVariant } from './TcnSkeleton.vue';

Last updated on July 24, 2026

Was this page helpful?