Skeletons

Customizable skeleton components for loading states. Supports various styles, layouts, and animation options. previews

Default Skeleton

A basic skeleton component with default settings.

 
<div class="rounded-md space-y-4">
  <div class="text-left">
    <div class="space-y-2">
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton with Title

A skeleton component with a title placeholder.

 
<div class="rounded-md space-y-4">
  <div class="text-left">
    <div
      class="rounded w-full mb-4 mr-auto h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div class="space-y-2">
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton title />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Full Skeleton Layout

A skeleton component with all elements: image, media, title, and content.

 
<div class="rounded-md space-y-4">
  <div class="text-left">
    <div
      class="rounded w-full mb-4 h-48 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div
      class="rounded-full mx-auto mb-4 h-24 w-24 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div
      class="rounded w-full mb-4 mr-auto h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div class="space-y-2">
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton image mediaPlaceholder title content />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton with Image

A skeleton component with an image placeholder.

 
<div class="rounded-md space-y-4">
  <div class="text-left">
    <div
      class="rounded w-full mb-4 h-48 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div class="space-y-2">
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton image />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton with Media Placeholder

A skeleton component with a circular media placeholder.

 
<div class="rounded-md space-y-4">
  <div class="text-left">
    <div
      class="rounded-full mx-auto mb-4 h-24 w-24 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div class="space-y-2">
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton mediaPlaceholder />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton Alignments

Skeletons with different content alignments.

 
<div class="space-y-4">
  <div class="rounded-md space-y-4">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-4">
    <div class="text-center">
      <div
        class="rounded w-full mb-4 mx-auto h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mx-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mx-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mx-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-4">
    <div class="text-right">
      <div
        class="rounded w-full mb-4 ml-auto h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full ml-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full ml-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full ml-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton title content alignment="left" />
<Skeleton title content alignment="center" />
<Skeleton title content alignment="right" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton Colors and Shades

Skeletons with different color schemes and shades.

 
<div class="space-y-8">
  <div class="rounded-md space-y-4">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-10 bg-slate-700 dark:bg-slate-900 text-white dark:text-white hover:text-slate-200 dark:hover:text-slate-300 border-slate-800 dark:border-slate-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-4 bg-slate-700 dark:bg-slate-900 text-white dark:text-white hover:text-slate-200 dark:hover:text-slate-300 border-slate-800 dark:border-slate-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-slate-700 dark:bg-slate-900 text-white dark:text-white hover:text-slate-200 dark:hover:text-slate-300 border-slate-800 dark:border-slate-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-slate-700 dark:bg-slate-900 text-white dark:text-white hover:text-slate-200 dark:hover:text-slate-300 border-slate-800 dark:border-slate-700"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-4">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-10 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-100 border-slate-200 dark:border-slate-600"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-4 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-100 border-slate-200 dark:border-slate-600"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-100 border-slate-200 dark:border-slate-600"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-100 border-slate-200 dark:border-slate-600"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-4">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-10 bg-blue-700 dark:bg-blue-900 text-white dark:text-white hover:text-blue-200 dark:hover:text-blue-300 border-blue-800 dark:border-blue-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-4 bg-blue-700 dark:bg-blue-900 text-white dark:text-white hover:text-blue-200 dark:hover:text-blue-300 border-blue-800 dark:border-blue-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-blue-700 dark:bg-blue-900 text-white dark:text-white hover:text-blue-200 dark:hover:text-blue-300 border-blue-800 dark:border-blue-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-blue-700 dark:bg-blue-900 text-white dark:text-white hover:text-blue-200 dark:hover:text-blue-300 border-blue-800 dark:border-blue-700"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-4">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-10 bg-blue-100 dark:bg-blue-800 hover:bg-blue-200 dark:hover:bg-blue-700 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-4 bg-blue-100 dark:bg-blue-800 hover:bg-blue-200 dark:hover:bg-blue-700 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-blue-100 dark:bg-blue-800 hover:bg-blue-200 dark:hover:bg-blue-700 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-blue-100 dark:bg-blue-800 hover:bg-blue-200 dark:hover:bg-blue-700 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600"></div>
      </div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton title content color="slate" shade="hard" />
<Skeleton title content color="slate" shade="soft" />
<Skeleton title content color="blue" shade="hard" />
<Skeleton title content color="blue" shade="soft" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton Shade Options

Skeletons with different shade options: soft and hard.

 
<div class="space-y-4">
  <div class="rounded-md space-y-4">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-10 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 border-gray-200 dark:border-gray-600"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-4 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 border-gray-200 dark:border-gray-600"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 border-gray-200 dark:border-gray-600"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 border-gray-200 dark:border-gray-600"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-4">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton title content shade="soft" />
<Skeleton title content shade="hard" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Animated Skeleton

A skeleton component with animation.

 
<div class="rounded-md animate-pulse space-y-4">
  <div class="text-left">
    <div
      class="rounded w-full mb-4 mr-auto h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div class="space-y-2">
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div
        class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton title content animate />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Custom Styled Skeleton

A skeleton with custom CSS classes applied.

 
<div class="space-y-4 bg-gradient-to-r from-purple-100 to-pink-100 p-4 rounded-lg shadow-md">
  <div class="text-left">
    <div
      class="rounded w-full mb-4 mr-auto h-10 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700 bg-purple-200"></div>
    <div class="space-y-2">
      <div
        class="rounded w-full mr-auto h-4 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700 bg-pink-200"></div>
      <div
        class="rounded w-full mr-auto h-4 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700 bg-pink-200"></div>
      <div
        class="rounded w-full mr-auto h-4 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700 bg-pink-200"></div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton
title
content
wrapperClass="bg-gradient-to-r from-purple-100 to-pink-100 p-4 rounded-lg shadow-md"
titleClass="bg-purple-200"
contentClass="bg-pink-200"
/>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton Table Layout

A skeleton component representing a table layout.

 
<div class="rounded-md space-y-4">
  <div class="space-y-2">
    <div
      class="w-full rounded h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"
      style="opacity: 0.7"></div>
    <div
      class="w-full rounded h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div
      class="w-full rounded h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div
      class="w-full rounded h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton layout="table" rows={4} />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton Form Layout

A skeleton component representing a form layout.

 
<div class="rounded-md space-y-4">
  <div class="space-y-4">
    <div
      class="rounded h-10 w-1/2 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div
      class="rounded h-10 w-3/4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div
      class="rounded h-10 w-full bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div
      class="rounded h-20 w-full bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
    <div
      class="rounded h-10 w-1/2 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton layout="form" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton with Image on Left

A skeleton component with an image on the left and content on the right.

 
<div class="rounded-md space-y-4">
  <div class="flex gap-4">
    <div
      class="rounded w-full mb-4 h-48 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"
      style="flex: 0 0 33%"></div>
    <div class="flex-1 space-y-2">
      <div
        class="rounded w-full mb-4 h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton layout="imageLeft" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton with Image on Right

A skeleton component with an image on the right and content on the left.

 
<div class="rounded-md space-y-4">
  <div class="flex flex-row-reverse gap-4">
    <div
      class="rounded w-full mb-4 h-48 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"
      style="flex: 0 0 33%"></div>
    <div class="flex-1 space-y-2">
      <div
        class="rounded w-full mb-4 h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton layout="imageRight" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Skeleton Sizes

Skeletons with different sizes.

 
<div class="space-y-12">
  <div class="rounded-md space-y-1">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-6 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-2 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-2 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-2 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-2">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-8 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-3 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-3 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-3 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-4">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-10 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-4 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-6">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-12 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-5 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-5 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-5 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
  <div class="rounded-md space-y-8">
    <div class="text-left">
      <div
        class="rounded w-full mb-4 mr-auto h-14 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      <div class="space-y-2">
        <div
          class="rounded w-full mr-auto h-6 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-6 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
        <div
          class="rounded w-full mr-auto h-6 bg-gray-700 dark:bg-gray-900 text-white dark:text-white hover:text-gray-200 dark:hover:text-gray-300 border-gray-800 dark:border-gray-700"></div>
      </div>
    </div>
  </div>
</div>
---
import { Skeleton } from '@/components/ui/skeleton';

---

<Skeleton title content size="xs" />
<Skeleton title content size="sm" />
<Skeleton title content size="md" />
<Skeleton title content size="lg" />
<Skeleton title content size="xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { twMerge } from "tailwind-merge";
import {
  colorPalette,
  type ColorName,
  getHardClasses,
  getSoftClasses,
} from "@utils/colorUtils";

// PropTypes for documentation
export const propTypes = {
  title: {
    type: "boolean",
    description: "Whether to show a title skeleton",
    default: false,
  },
  content: {
    type: "boolean",
    description: "Whether to show content skeleton",
    default: true,
  },
  alignment: {
    type: ["left", "center", "right"],
    description: "Alignment of the skeleton content",
    default: "left",
  },
  image: {
    type: "boolean",
    description: "Whether to show an image skeleton",
    default: false,
  },
  mediaPlaceholder: {
    type: "boolean",
    description: "Whether to add a media placeholder",
    default: false,
  },
  animate: {
    type: "boolean",
    description: "Whether to animate the skeleton",
    default: false,
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color scheme for the skeleton",
    default: "gray",
  },
  shade: {
    type: ["soft", "hard"],
    description: "Shade of the skeleton color",
    default: "hard",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the skeleton",
  },
  rows: {
    type: "number",
    description: "Number of rows for table or form layout",
    default: 3,
  },
  layout: {
    type: ["default", "table", "form", "imageLeft", "imageRight"],
    description: "Layout type for the skeleton",
    default: "default",
  },
  wrapperClass: {
    type: "string",
    description: "Custom CSS classes for the wrapper element",
    default: "",
  },
  titleClass: {
    type: "string",
    description: "Custom CSS classes for the title element",
    default: "",
  },
  contentClass: {
    type: "string",
    description: "Custom CSS classes for the content elements",
    default: "",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl"],
    description: "Size of the skeleton",
    default: "md",
  },
};

// Types and Interfaces
type SkeletonVariants = VariantProps<typeof skeletonStyles>;

interface Props extends HTMLAttributes<"div">, SkeletonVariants {
  title?: boolean;
  content?: boolean;
  alignment?: "left" | "center" | "right";
  image?: boolean;
  mediaPlaceholder?: boolean;
  animate?: boolean;
  color?: ColorName;
  shade?: "soft" | "hard";
  class?: string;
  rows?: number;
  layout?: "default" | "table" | "form" | "imageLeft" | "imageRight";
  wrapperClass?: string;
  titleClass?: string;
  contentClass?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
}

// Component Logic
const {
  title = false,
  content = true,
  alignment = "left",
  image = false,
  mediaPlaceholder = false,
  animate = false,
  color = "gray",
  shade = "hard",
  class: className = "",
  rows = 3,
  layout = "default",
  wrapperClass = "",
  titleClass = "",
  contentClass = "",
  size = "md",
} = Astro.props as Props;

// Styles
const skeletonStyles = tv({
  base: "rounded-md",
  variants: {
    animate: {
      true: "animate-pulse",
      false: "",
    },
    size: {
      xs: "space-y-1",
      sm: "space-y-2",
      md: "space-y-4",
      lg: "space-y-6",
      xl: "space-y-8",
    },
  },
  defaultVariants: {
    animate: false,
    size: "md",
  },
});

const itemStyles = tv({
  base: "rounded w-full",
  variants: {
    type: {
      title: "mb-4",
      content: "",
      image: "mb-4",
      media: "rounded-full mx-auto mb-4",
    },
    alignment: {
      left: "mr-auto",
      center: "mx-auto",
      right: "ml-auto",
    },
    size: {
      xs: "",
      sm: "",
      md: "",
      lg: "",
      xl: "",
    },
  },
  compoundVariants: [
    // Title sizes
    { type: "title", size: "xs", class: "h-6" },
    { type: "title", size: "sm", class: "h-8" },
    { type: "title", size: "md", class: "h-10" },
    { type: "title", size: "lg", class: "h-12" },
    { type: "title", size: "xl", class: "h-14" },
    // Content sizes
    { type: "content", size: "xs", class: "h-2" },
    { type: "content", size: "sm", class: "h-3" },
    { type: "content", size: "md", class: "h-4" },
    { type: "content", size: "lg", class: "h-5" },
    { type: "content", size: "xl", class: "h-6" },
    // Image sizes
    { type: "image", size: "xs", class: "h-32" },
    { type: "image", size: "sm", class: "h-40" },
    { type: "image", size: "md", class: "h-48" },
    { type: "image", size: "lg", class: "h-56" },
    { type: "image", size: "xl", class: "h-64" },
    // Media sizes
    { type: "media", size: "xs", class: "h-16 w-16" },
    { type: "media", size: "sm", class: "h-20 w-20" },
    { type: "media", size: "md", class: "h-24 w-24" },
    { type: "media", size: "lg", class: "h-32 w-32" },
    { type: "media", size: "xl", class: "h-40 w-40" },
  ],
});

const tableRowStyles = tv({
  base: "w-full rounded",
  variants: {
    size: {
      xs: "h-6",
      sm: "h-8",
      md: "h-10",
      lg: "h-12",
      xl: "h-14",
    },
  },
});

const formRowStyles = tv({
  base: "rounded",
  variants: {
    type: {
      short: "h-10 w-1/2",
      medium: "h-10 w-3/4",
      long: "h-10 w-full",
      textarea: "h-20 w-full",
    },
  },
});

// Helper function to get the appropriate color classes
const getColorClasses = (color: ColorName, shade: "soft" | "hard") => {
  return shade === "soft" ? getSoftClasses(color) : getHardClasses(color);
};

// Helper function to merge classes
const mergeClasses = (baseClasses: string, customClasses: string) => {
  return twMerge(baseClasses, customClasses);
};
---

<div class={mergeClasses(skeletonStyles({ animate, size }), wrapperClass)}>
  {
    layout === "default" && (
      <div class={`text-${alignment}`}>
        {image && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "image", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {mediaPlaceholder && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "media", size }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        )}
        {title && (
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", alignment, size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
        )}
        {content && (
          <div class="space-y-2">
            {[...Array(rows)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", alignment, size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        )}
      </div>
    )
  }

  {
    layout === "table" && (
      <div class="space-y-2">
        <div
          class={mergeClasses(
            twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
            contentClass,
          )}
          style="opacity: 0.7;"
        />
        {[...Array(rows - 1)].map((_, index) => (
          <div
            class={mergeClasses(
              twMerge(tableRowStyles({ size }), getColorClasses(color, shade)),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "form" && (
      <div class="space-y-4">
        {["short", "medium", "long", "textarea", "short"].map((type) => (
          <div
            class={mergeClasses(
              twMerge(
                formRowStyles({ type: type as any }),
                getColorClasses(color, shade),
              ),
              contentClass,
            )}
          />
        ))}
      </div>
    )
  }

  {
    layout === "imageLeft" && (
      <div class={`flex gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }

  {
    layout === "imageRight" && (
      <div class={`flex flex-row-reverse gap-4`}>
        <div
          class={mergeClasses(
            twMerge(
              itemStyles({ type: "image", size }),
              getColorClasses(color, shade),
            ),
            contentClass,
          )}
          style="flex: 0 0 33%;"
        />
        <div class="flex-1 space-y-2">
          <div
            class={mergeClasses(
              twMerge(
                itemStyles({ type: "title", size }),
                getColorClasses(color, shade),
              ),
              titleClass,
            )}
          />
          <div class="space-y-2">
            {[...Array(3)].map(() => (
              <div
                class={mergeClasses(
                  twMerge(
                    itemStyles({ type: "content", size }),
                    getColorClasses(color, shade),
                  ),
                  contentClass,
                )}
              />
            ))}
          </div>
        </div>
      </div>
    )
  }
</div>

Component Properties

Property Type Default Description
title boolean false Whether to show a title skeleton
content boolean true Whether to show content skeleton
alignment left | center | right "left" Alignment of the skeleton content
image boolean false Whether to show an image skeleton
mediaPlaceholder boolean false Whether to add a media placeholder
animate boolean false Whether to animate the skeleton
color white | slate | gray | zinc | neutral | stone | red | orange | amber | yellow | lime | green | emerald | teal | cyan | sky | blue | indigo | violet | purple | fuchsia | pink | rose "gray" Color scheme for the skeleton
shade soft | hard "hard" Shade of the skeleton color
class string - Additional CSS classes to apply to the skeleton
rows number 3 Number of rows for table or form layout
layout default | table | form | imageLeft | imageRight "default" Layout type for the skeleton
wrapperClass string "" Custom CSS classes for the wrapper element
titleClass string "" Custom CSS classes for the title element
contentClass string "" Custom CSS classes for the content elements
size xs | sm | md | lg | xl "md" Size of the skeleton