---
title: Improving Turbo Frame UX with the busy Attribute
author: Victor Cobos
tags: Turbo, Rails
published_at: '2025-10-07T23:45:00+02:00'
updated_at: '2025-10-08T00:00:43+02:00'
canonical_url: https://y8xqp.hatchboxapp.com/articles/improving-turbo-frame-ux-with-the-busy-attribute
---

# Improving Turbo Frame UX with the busy Attribute

Turbo Frames don’t just swap content — they also tell you when they’re working. The busy attribute appears during a load, and with a little CSS you can turn it into spinners, skeletons, or subtle fades. In this post I’ll show simple patterns to hook into busy and give users clear feedback while they wait.

When you drop a `<turbo-frame>` into your Rails app, the default experience is pretty bare. Click a link, and the frame quietly empties itself while waiting for the new content. For a second or two, the user just sees a blank box. Nothing is broken, but the silence feels awkward: is the app still working? did my click register?

That tiny gap is where UX can stumble. Without a loading indicator, users are left guessing what’s happening behind the scenes.

Luckily, Turbo gives us a built-in signal for this state. Whenever a <turbo-frame> is fetching new content, it automatically adds two attributes to the element:

* `busy` – a simple attribute with no value
*  `aria-busy="true"` – an accessibility attribute used by assistive technologies

When the request finishes, Turbo removes busy and flips `aria-busy` back to `false`.

In other words, your frame is already telling the browser — and you — when it’s busy. All we need to do is style that state to give users a clear loading indicator. You can read more about the full list of frame attributes in the [Turbo Frames documentation](https://turbo.hotwired.dev/reference/frames#html-attributes).

## Styling the aria-busy state

Since Turbo keeps `aria-busy` up to date on every frame, that’s the attribute we’ll use for styling. The good news: Tailwind already includes [`aria-busy:*` variants](https://tailwindcss.com/docs/hover-focus-and-other-states#aria-states), so we can react to this state directly in our markup — no JavaScript needed.

In the examples below, I’ll use Tailwind utilities because they make it easy to demonstrate the concept. But the same idea works perfectly with plain CSS using the attribute selector:

```css
turbo-frame[aria-busy="true"] {
	...
}
```

There are two common patterns:
1. 	Showing an overlay inside the frame: `group-aria-busy`
2.	Placing a loader as a sibling overlay `peer-aria-busy`

## Inside the frame — using group-aria-busy

This approach places the loading indicator inside the `<turbo-frame>` itself.
We mark the frame as a group, which lets us style its children when it’s busy.
When Turbo sets `aria-busy="true"`, the content dims and a spinner overlay fades in.

```html
<turbo-frame class="group relative">
  <div class="hidden group-aria-busy:grid pointer-events-none absolute inset-0 place-items-center bg-white/60 backdrop-blur-sm rounded-lg">
    <div class="size-6 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
  </div>
</turbo-frame>
```

[turbo_frame_group_aria_busy demo](https://y8xqp.hatchboxapp.com/demos/turbo_frame_group_aria_busy)

When the frame is idle, the overlay is hidden.
As soon as Turbo marks it `aria-busy="true"`, the spinner appears and the content fades slightly to signal activity.

## Outside the frame — using peer-aria-busy

Sometimes, you may want the loader outside the frame — for instance, if you don’t want to modify the frame’s internal markup or you want a larger overlay.

In this case, we wrap the frame in a container and mark the frame as a `peer`.
That lets sibling elements react to its `aria-busy` state using `peer-aria-busy:` utilities.

```html
<div class="relative">
  <turbo-frame class="peer"></turbo-frame>
  <div class="hidden peer-aria-busy:grid pointer-events-none absolute inset-0 place-items-center bg-white/60 backdrop-blur-sm rounded-lg">
    <div class="size-6 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
  </div>
</div>
```

[turbo_frame_peer_aria_busy demo](https://y8xqp.hatchboxapp.com/demos/turbo_frame_peer_aria_busy)

Here the overlay sits next to the frame, but still reacts to its busy state.
This pattern keeps your frame markup clean while still providing clear visual feedback.

## Global overlay with ::before/::after (no extra markup)

You can style every `<turbo-frame>` busy state with a single utility string — no sibling/child elements needed. The `::before` pseudo-element becomes the overlay and shows a centered spinner background.

```html
<turbo-frame class="group relative block aria-busy:pointer-events-none before:absolute aria-busy:before:inset-0 aria-busy:before:grid before:place-items-center before:bg-white/60 before:backdrop-blur-sm before:rounded-lg before:bg-[url('icons/spinner/180-ring-with-bg.svg')] before:bg-no-repeat before:bg-size-[5%] before:bg-center"></turbo-frame>
```

[turbo_frame_before_aria_busy demo](https://y8xqp.hatchboxapp.com/demos/turbo_frame_before_aria_busy)

## Wrapping up

Turbo Frames make it easy to update parts of a page without a full reload, but that same magic can feel confusing when users don’t see what’s happening. The `busy` and `aria-busy` attributes give us a lightweight, built-in way to surface that hidden state — no custom JavaScript required.

Whether you show a subtle spinner, a blurred overlay, or a full skeleton loader, the idea is the same: **keep users informed**. A short visual cue turns a silent wait into a smoother, more trustworthy experience.

And while the examples here use Tailwind’s `aria-busy` variants for convenience, the same concept works anywhere you can write CSS. Turbo does the signaling; you decide how it looks.

## References

* [Hotwire Turbo Frames: Add Seamless Loading Spinners in Rails](https://unagisoftware.com/articles/hotwire-turbo-frames-add-seamless-loading-spinners-in-rails/)
* [How to add a loading animation to your turbo frame with TailwindCSS](https://blog.arkency.com/how-to-add-a-loading-animation-to-your-turbo-frame-with-tailwindcss/)
