All Posts

How to create loading spinners in Tailwind CSS

Loading spinners tell users something is happening in the background. Maybe you're fetching data from an API, processing a form submission, or loading the next page.

Without a spinner, users wonder if their click registered. They might click again, reload the page, or just leave. Not good.

The good news is that Tailwind CSS makes creating spinners incredibly simple. You don't need external libraries or complex animations - just a few utility classes and you're done.

The basic border spinner

The most common spinner is the rotating circle. You've seen it everywhere - Gmail, Twitter, pretty much any modern web app.

Here's how to build one in Tailwind. You need a div with borders and the animate-spin utility:

<div class="size-8 border-4 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>

Let me break down what's happening here.

The size-8 gives us both width and height. The border-4 creates the circle outline. Then border-gray-300 sets a light gray base color for the entire circle.

Here's the trick: border-t-blue-500 overrides just the top border with blue. When the div spins with animate-spin, that blue section creates the spinner effect.

The rounded-full is what turns our square div into a circle.

You can change colors to match your brand:

<div class="size-8 border-4 border-gray-200 border-t-green-600 rounded-full animate-spin"></div>
<div class="size-8 border-4 border-gray-200 border-t-purple-600 rounded-full animate-spin"></div>
<div class="size-8 border-4 border-gray-200 border-t-red-600 rounded-full animate-spin"></div>

That's the foundation. Simple, clean, and it works.

Customizing spinner size and speed

Spinners come in all sizes depending on where you're using them. A tiny spinner for an inline button. A big one for a full-page loading screen.

Tailwind makes sizing dead simple. Just swap out the size utility:

<!-- Small spinner -->
<div class="size-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>

<!-- Medium spinner -->
<div class="size-8 border-4 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>

<!-- Large spinner -->
<div class="size-16 border-4 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>

Notice I adjusted the border width too. Smaller spinners look better with thinner borders (border-2), while larger ones can handle thicker borders.

Now for speed. By default, animate-spin does one full rotation per second. Sometimes that's too fast or too slow.

You can control the speed with custom animation durations. Here's how to make a slower spinner using arbitrary values:

<!-- Slower spinner (2 second rotation) -->
<div class="size-8 border-4 border-gray-300 border-t-blue-500 rounded-full animate-spin [animation-duration:2s]"></div>

<!-- Faster spinner (0.5 second rotation) -->
<div class="size-8 border-4 border-gray-300 border-t-blue-500 rounded-full animate-spin [animation-duration:0.5s]"></div>

The arbitrary value syntax [animation-duration:2s] lets you set any duration you want without writing custom CSS.

Different spinner styles

The border spinner is great, but Tailwind gives you other animation utilities that work well for loading states.

Let's look at a few alternatives.

Dotted spinner

Instead of a solid border, you can use a dotted border for a different look:

<div class="size-8 border-4 border-dotted border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>

The border-dotted class creates those dots. You can also try border-dashed for dashes instead.

Ping effect

For situations where you want a pulsing radar effect, use animate-ping:

<div class="relative inline-flex">
  <div class="size-3 bg-blue-500 rounded-full"></div>
  <div class="absolute size-3 bg-blue-500 rounded-full animate-ping"></div>
</div>

The ping animation scales up and fades out. Great for showing active connections or live updates.

Pulse effect

The animate-pulse utility creates a fade in and out effect:

<div class="flex gap-2">
  <div class="size-2 bg-blue-500 rounded-full animate-pulse"></div>
  <div class="size-2 bg-blue-500 rounded-full animate-pulse [animation-delay:0.2s]"></div>
  <div class="size-2 bg-blue-500 rounded-full animate-pulse [animation-delay:0.4s]"></div>
</div>

By staggering the animation delay on each dot, you get that classic "thinking" loader effect.

Adding spinners to buttons

This is where spinners really shine. When a user clicks a submit button, you want to show them something is happening.

Here's a button with an inline spinner:

<button class="px-6 py-2 bg-blue-500 text-white rounded-lg flex items-center gap-2">
  <div class="size-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
  Loading...
</button>

The key here is using flex items-center gap-2 to align the spinner with the text. I also used border-white/30 for the base border so it blends nicely with the button background.

Sometimes you want the spinner to replace the button text entirely:

<!-- Normal state -->
<button class="px-6 py-2 bg-blue-500 text-white rounded-lg">
  Submit
</button>

<!-- Loading state -->
<button class="px-6 py-2 bg-blue-500 text-white rounded-lg flex items-center justify-center" disabled>
<div class="size-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
</button>

When loading, swap the text for a spinner and add the disabled attribute. This prevents users from clicking multiple times.

For icon buttons, center the spinner like this:

<button class="size-10 bg-blue-500 text-white rounded-lg flex items-center justify-center">
  <div class="size-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
</button>

Perfect for icon-only buttons where you need a compact loading state.

Making spinners accessible

Spinners are visual elements, which means screen readers can't see them. You need to help assistive technologies understand what's happening.

Add role="status" and include text for screen readers:

<div class="flex items-center gap-2" role="status">
  <div class="size-6 border-4 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
  <span class="sr-only">Loading...</span>
</div>

The sr-only class hides the text visually but keeps it available for screen readers. This is way better than leaving users guessing.

For buttons with spinners, add aria-label when the button text disappears:

<button
  class="px-6 py-2 bg-blue-500 text-white rounded-lg flex items-center justify-center"
  disabled
  aria-label="Submitting form"
>
  <div class="size-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
</button>

Now screen reader users hear "Submitting form, please wait" instead of just silence.

One more thing: don't rely only on the spinner. If the loading takes more than a couple seconds, show a message. Users appreciate knowing what's happening, especially if there's a delay.


That's everything you need to know about creating spinners in Tailwind CSS. From basic rotating circles to pulsing dots to button loading states, you now have the tools to show users when something's happening.

Remember to keep accessibility in mind. Your sighted users see the spinner, but screen reader users need text descriptions too.

Now go build some smooth loading experiences!

Other posts you might like...

See more

Subscribe to our newsletter

Get notified when we add new templates, components, and more!

Thanks for subscribing!
Please check your email to confirm your subscription.

WindyBase Make development a breeze

WindyBase is a weekly curated Tailwind CSS template and tool directory built for the modern developer.

© 2025 WindyBase. All rights reserved.