Simple Skeleton Styling with Tailwind
- Mitchell Christ

Loading states in web applications can make or break the user experience. While spinners and loading indicators have their place, skeleton screens often provide a more elegant and less jarring loading experience. In SanityPress, we've implemented a simple yet powerful approach to skeleton loading using Tailwind CSS custom utilities.
The Implementation¶
Our approach combines Tailwind's plugin system with empty pseudo-selectors to create skeleton states that automatically appear when content is loading. Here's how we've set it up:
1. Tailwind Configuration¶
First, we define a custom line height unit and create a skeleton utility in our Tailwind configuration:
import plugin from 'tailwindcss/plugin'
import type { Config } from 'tailwindcss'
export default {
theme: {
// ...
lh: {
DEFAULT: '1lh', // line height unit
2: '2lh',
3: '3lh',
// set more if needed
},
},
plugins: [
plugin(function ({ matchUtilities, theme }) {
matchUtilities(
{
skeleton: (value) => ({
height: value,
backgroundColor: theme('colors.neutral.50'),
}),
},
{
values: theme('lh'),
},
)
}),
],
} satisfies Config
2. Frontend Implementation¶
With our utility configured, implementing skeleton states becomes remarkably simple:
export default function Page() {
return (
<section>
<Suspense fallback={<MyModule skeleton />}>
<MyModule />
</Suspense>
</section>
)
}
export default function MyModule({ skeleton, ...props }) {
return (
<div className="space-y-4">
<h1 className={cn(skeleton && 'empty:skeleton')}>{props.title}</h1>
<p className={cn('line-clamp-3', skeleton && 'empty:skeleton-3')}>
{props.description}
</p>
{/* add background to image wrappers as a fallback */}
<figure className="aspect-video bg-neutral-50">
<Img
className="aspect-video w-full object-cover"
image={props.image}
alt="..."
/>
</figure>
</div>
)
}
How It Works¶
The magic happens through the combination of three key features:
- React Suspense: We wrap our components in a
<Suspense>
component, which allows us to show a fallback UI while the component's data is loading. - Define line height units: We define line height units (
1lh
,2lh
,3lh
) that correspond to the number of text lines we want our skeleton to represent. - Skeleton utility: Our custom
skeleton
utility applies both the height (based on line height units) and a subtle background color. - Empty pseudo-class: The
empty:
modifier applies styles only when an element has no content, making our skeleton states appear automatically during loading.
Benefits¶
This approach offers several advantages:
- Automatic: Skeleton states appear naturally when content is loading.
- Maintainable: No separate skeleton components needed.
- Flexible: Easy to adjust the number of lines for different content lengths.
- Performant: Minimal CSS output thanks to Tailwind's utility-first approach.
Conclusion¶
By leveraging Tailwind's plugin system and :empty
pseudo-selectors, we've created a clean and maintainable solution for skeleton loading states in SanityPress. This approach provides a smooth loading experience while keeping our codebase simple and maintainable.
Try it out in your own projects, and let us know how it works for you!
