Skeletons
Customizable skeleton components for loading states. Supports various styles, layouts, and animation options. previews<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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 |