The Problem 💡
Have you ever encountered a scenario where you needed to implement a loading state for a button, but clicking the button caused it to change size?

This behavior can look jittery, unprofessional, and not user-friendly. It also introduces layout shifts, which negatively impact your website’s user experience and SEO performance. So, how do you prevent this from happening? How should you approach this problem?
There are two main approaches to solving this issue, but one stands out as the better option. Let’s explore both.
- Absolute positioning approach
- Grid stacking approach
Initial approach
Initially, you might think to render text by default and, when the loading state is active, replace the text with a spinner. Here’s how you can create a basic button component:
import Spinner from './spinner';
type TProps = {
text: string;
isLoading?: boolean;
onClick: () => void;
};
const Button = ({ text, isLoading, onClick }: TProps) => {
return (
<button
onClick={onClick}
className='px-4 py-2 bg-purple-500 text-white flex justify-between items-center'
>
{!isLoading ? <span>{text}</span> : <Spinner />}
</button>
);
};
export default Button;As you can see, this is a straightforward button component with an isLoading property that determines whether the text or spinner is rendered.
'use client';
import Button from './components/button';
import { useState } from 'react';
export default function Home() {
const [isLoading, setIsLoading] = useState(false);
const handleLoading = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 3000);
};
return (
<main className='grid place-items-center h-screen'>
<Button
text='Submit this form'
onClick={() => handleLoading()}
isLoading={isLoading}
/>
</main>
);
}Here’s how you would display the button component.
However, this approach leads to a resizing problem: when text is displayed, the button has a wider size, and when the loading state activates, the button becomes narrower. This is where we need to rethink our solution.
1. Absolute Positioning Approach 📊
To address the resizing issue, you can use absolute positioning. This requires a few adjustments inside the button and spinner components.
import Spinner from './spinner';
type TProps = {
text: string;
isLoading?: boolean;
onClick: () => void;
};
const Button = ({ text, isLoading, onClick }: TProps) => {
return (
<button
onClick={onClick}
className='px-4 py-2 bg-purple-500 text-white flex justify-between items-center relative'
>
<span className={`${isLoading ? 'invisible' : 'visible'}`}>{text}</span>
{isLoading && (
<div className='absolute inset-0 w-full flex items-center justify-center'>
<Spinner />
</div>
)}
</button>
);
};
export default Button;The changes include adding the relative class to the button, modifying how button text is rendered during the loading state, and wrapping the Spinner component with an additional container.
But what’s the drawback of this approach? While it resolves the resizing issue, it introduces a new problem - text overflow.

As you can see, when you have a loading state message that is bigger than the initial button text, then you have a problem with text overflow. How can we possibly fix this problem?
2. Grid Stacking Approach 🌐
As shown here, if your loading state message is longer than the initial button text, it causes text overflow. To fix this, we need a better solution.
import Spinner from './spinner';
type TProps = {
text: string;
isLoading?: boolean;
onClick: () => void;
};
const Button = ({ text, isLoading, onClick }: TProps) => {
return (
<button
onClick={onClick}
className={`px-4 py-2 bg-purple-500 text-white grid [grid-template-areas:stack]`}
>
<span
className={`[grid-area:stack] ${!isLoading ? 'visible' : 'invisible'}`}
>
{text}
</span>
<Spinner
className={`[grid-area:stack] place-self-center ${
isLoading ? 'visible' : 'invisible'
}`}
/>
</button>
);
};
export default Button;First, you need to add grid as a className to the button, enabling CSS Grid layout. The key className here is [grid-template-areas:stack], which defines a single grid area called stack. This ensures all child elements stack on top of each other.
To make grid-areas work, apply [grid-area:stack] to both the button text and the Spinner component. Additionally, use classNames like visible and invisible to toggle the visibility of the elements as needed.

The spinner output remains identical to the absolute positioning approach. However, grid stacking offers a significant advantage.

As illustrated, the grid-based approach considers the largest child element when determining the button size. This means the button’s initial size already accommodates the longer loading message, preventing layout shifts.
Conclusion 🔧
This blog post has detailed two approaches for addressing button resizing issues during a loading state: absolute positioning and grid stacking. While absolute positioning offers a quick fix, grid stacking emerges as the superior solution by avoiding text overflow and layout shifts, thus improving user experience and maintaining SEO performance.
At AS Agency, we meticulously address these nuances to ensure our clients’ websites are polished, user-friendly, and optimized for the best performance. If you’re looking for a team that prioritizes every small detail to deliver exceptional results, look no further. Let’s create something amazing together! ✨

Napisao Antonio Šumak
Osnivač AS Agency. Dizajniram i izrađujem brze web stranice za male tvrtke — i zapisujem ono što naučim usput.


