- Introduction
- Understanding the problem
- The plan
- The code
- Key concepts
- Advantages of this approach
- Tailwind CSS breakdown
- Comparison with the previous approach
- Conclusion
Introduction
In a previous post, I demonstrated how to build a loading button in React using useLayoutEffect
to calculate and preserve the button’s width while displaying a loading spinner. While that approach works well, it relies on JavaScript for layout calculations. In this post, I’ll show you an alternative technique using pure CSS for the loading indicator, leveraging Tailwind CSS’s group
and group-data
utilities to style elements based on the parent’s state. This solution was inspired by a post from jordi on X.
Understanding the problem
When building a loading button, one common issue is the button’s content changing width when the loading indicator is displayed. In the first version, we solved this by dynamically calculating the button’s width with JavaScript. However, this approach can be avoided by creatively using CSS opacity and Tailwind’s state-based utilities.
The plan
In this version, we’ll:
- Use Tailwind CSS classes to style the button and loading indicator.
- Use the
data-loading
attribute on the button to toggle styles. - Ensure smooth transitions between the button’s normal state and its loading state without any JavaScript-based width calculations.
The code
import { LoaderCircleIcon } from 'lucide-react';
import { FC } from 'react';
import { Button, ButtonProps } from '@/components/ui/button';
import { cn } from '@/utils/shadcn';
interface LoadingButtonProps extends ButtonProps {
isLoading?: boolean;
}
export const LoadingButton: FC<LoadingButtonProps> = (props) => {
const {
isLoading = false,
disabled,
className,
children,
...restOfProps
} = props;
return (
<Button
{...restOfProps}
data-loading={isLoading}
className={cn(className, 'group')}
disabled={disabled ?? isLoading}>
<LoaderCircleIcon className="absolute animate-spin opacity-0 group-data-[loading=true]:opacity-100" />
<span className="flex size-full items-center justify-center group-data-[loading=true]:opacity-0">
{children}
</span>
</Button>
);
};
Key concepts
The data-loading attribute
Instead of using JavaScript to manipulate styles directly, we’re adding a data-loading
attribute to the button. This attribute acts as a hook for styling elements inside the button.
Tailwind CSS group utilities
We use the group
class on the button and the group-data-[loading=true]
utility to style child elements based on the button’s data-loading
state. For example:
- The loading spinner (a
<LoaderCircleIcon>
component from Lucide) is normally hidden (opacity-0
) and becomes visible (opacity-100
) when thedata-loading
attribute istrue
. - The button’s content (
<span>
) is visible by default but hidden (opacity-0
) in the loading state.
Absolute positioning
The loading spinner is absolutely positioned within the button, ensuring it remains centered regardless of the button’s content.
Advantages of this approach
- No JavaScript layout calculations: By using CSS for state-based styling, we avoid unnecessary JavaScript logic.
- Simpler state management: The
isLoading
prop directly maps to the data-loading attribute, making it easier to manage. - Improved performance: CSS transitions are typically more performant than JavaScript-based DOM manipulations.
Tailwind CSS breakdown
Here’s a breakdown of the key Tailwind classes used:
group
: Enables state-based styling for child elements.group-data-[loading=true]
: Targets elements when thedata-loading
attribute is set totrue
.absolute
: Positions the spinner in the center of the button.animate-spin
: Adds a spinning animation to the loading icon.opacity-0
andopacity-100
: Toggles visibility of elements.
Comparison with the previous approach
Feature | Previous version | Current version |
---|---|---|
Width management | JavaScript (useLayoutEffect ) |
Pure CSS with opacity |
Styling | Inline and Tailwind CSS | Tailwind CSS group utilities |
Dependencies | Required DOM measurements | No additional dependencies |
Conclusion
This CSS-based approach simplifies the implementation of a loading button, avoiding JavaScript-based layout calculations while leveraging the power of Tailwind CSS. By using group
and group-data
, we achieve a clean and maintainable solution.
Try this technique in your next project and see how it compares to the JavaScript-based approach!