Simple Skeleton Styling with Tailwind

Read time: 2 minutes
Mitchell Christ
Mitchell Christ
t-rex skeleton holding a Macbook with the screen "SanityPress" in the museum of natural history, young kids in the foreground observing the exhibit, Hasselblad 4k

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:

tailwind.config.ts
  • 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:

    1. 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.
    2. 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.
    3. Skeleton utility: Our custom skeleton utility applies both the height (based on line height units) and a subtle background color.
    4. 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!

    skeletons waiting in line at the DMV with the signage "SanityPress DMV", Hasselblad 4k film still