

Do I really need to write an introduction to button components? We all know them. Buttons are one of the most commonly used components in our user interfaces, and are also some of the messiest components, with crappy interfaces and complex implementations for something as simple as a freaking button, and they only get worse as your codebase ages.
In this post, I’ll briefly explain why button components are a classic problem in software engineering, how composition can solve this problem, and how I implement complex buttons with simple code these days.
Buttons seem so simple at first, but we need a lot from them: Primary buttons, secondary buttons, red buttons for destructive actions, bordered buttons, transparent buttons. In forms we need them to be an actual HTML <button> element, sometimes as type="submit" and sometimes as type="button". Sometimes they aren’t buttons, they are links and should be an <a> element, unless it’s client-side routing where we need to use a special <Link> component. Buttons come in different sizes. They should support icons on the left, icons on the right, or only an icon and no text—but then of course with a tooltip! They can be disabled, but sometimes they can look disabled even though they are not actually disabled. They can have a loading state, so it also needs to support that A͟N͟D͟ T͟H͟E͟N S͟O͟M͟E͟T͟I͟M͟E͟S T̷H̴E̶ ̶B̸U̶T̶T̴O̵N̸S̶ 𝙏𝙃𝙀𝙔 𝘼𝙍𝙀 𝘽𝙐𝙏𝙏𝙊𝙉𝙎 𝘛𝙃𝘼𝙏 𝘼𝙍𝙀𝙉’𝙏¿!? ✖︎ B̷U̸T̶T̷O̴N̸S̶ — THEY REFUSE to exist ╰┈➤ t͞h̸e͟y͞ ͞m͡e͜l͜t͡ ͟i͠n͜t̛o̕ ̴t̕h̨e͝ ͘c̛o͞d͡e͢, THEY TALK <Button prop="kill_me" /> THE PROPS ARE SCREAMING AT ME > Warning: prop 'scream' is not recognized on the DOM element. Did you mean 'screm'? onClick={() => END_MY_SUFFERING()} ⠀⛓ ᴀɴᴅ ʏᴇᴛ ɪ sᴛɪʟʟ ʜᴀᴠᴇ ᴛᴏ ᴅᴇᴀʟ ᴡɪᴛʜ as="a"
…
It’s a lot.
Buttons have to address many concerns. And our solution is really to try to squeeze all of that into a single component? Really?
You might say: “hang on, we don’t always use a single component!” And you’re right—sometimes you’ll see separate components, such as an <IconButton>. However, I’d argue that this often makes things worse. Because now you have to implement many of those complex concerns twice. While you remove a single concern from your big bad button component (for example, the concern that it only renders an icon and no text), your new special <IconButton> still looks like a button, can be disabled and hovered like a button, and needs to share most of the same styles. You’ll also need to implement the polymorphism twice to render different clickable HTML elements.
This problem sounds familiar! It seems like we’re dealing with separation of concerns, and simply refactoring it into an <IconButton> is the wrong separation. We don’t usually solve separation of concerns like this—we solve it with layering and composition!
The button component is nothing more than a classic software engineering problem that we usually solve with composition… yet I’ve rarely seen it solved with good composition!
Forget the idea of using a single button component. From now on, we’ll think of every button in our UI as multiple layered components.
The base layer of every button is an unstyled, native interactive element, such as a button element or an anchor, in which we’ll nest a component that looks like a button:
We immediately and completely remove polymorphism from our button components. That’s already a huge improvement. Polymorphic components are a big pain in the ass, and I try to avoid them wherever possible.
Note that we won’t encapsulate our layered button into a single button component again. This would just result in a messy polymorphic component with lots of props and complexity. We will always call all layers as we need them.
We can now achieve a lot with pure CSS. Adding hover and disabled states is straightforward and elegant:
It’s great that we can solve this simply by adding CSS, without having to change any existing code. There’s no need to concatenate classes dynamically, nor did we need to add an attribute to our button.
However, if necessary, we can also add a attribute to our button component. This would be useful if we needed a button that looks disabled but isn’t.
I’ve seen some really messy solutions to this problem, such as two separate and attributes, where it’s unclear in which combination they should be used. But with composition, we don’t need to worry about that! We can immediately see from the way it’s used that we have a clickable button that isn’t disabled, and it has a visual child that looks disabled.
I cannot stress enough how powerful this is. We’re eliminating ambiguity and complexity while writing code that documents itself.
I’ve heard engineers in the past who argued that this approach is bad because you end up repeating yourself more. For example, writing many times. It’s often seen as a violation of the “Don’t repeat yourself” (DRY) principle.
But DRY doesn’t mean that you’re never allowed to repeat yourself. It’s not about avoiding repetition at any cost. It’s about avoiding duplication of logic and knowledge.
In our case, the repetition is fine. It’s markup. The alternative, like , is the same repetition in a worse syntax.
Now that our base is in place, we can start adding individual components for each concern and layer them into our button.
For example, let’s say we want to add a loading state to our buttons. This means:
Here’s how we can implement this by separating each concern into its own layer:
With composition, we were able to solve it without a single change to the . That’s a good thing! It shows that our composition is working well.
Another positive sign is that all of our attributes are in the right place. We added a and attribute, both of which are obvious in the context of the component. This new component addresses one specific concern, and all its attributes are related to this concern.
To illustrate a poor design choice, let’s assume that we would add the loading state concern into the . This would result in something like this:
This is a whack component interface! The attributes and are related to each other, but is not. All attributes have to be optional because not every button has a loading state. However, setting makes no sense without . The relationship between the attributes is not immediately obvious. This interface requires documentation.
If you have a whack component interface like this and your attributes are a mess of unrelated concerns, it’s a clear sign that you need to separate these concerns into different layers.
Until now, we have only used a single . In reality we often have different button variants, such as a in a different color, or a transparent button, or a red button for destructive actions.
We could simply create an attribute for this, which would be an acceptable solution. But I’ll explain why we might not want this:
According to what I wrote in the previous section, an attribute is sufficient for the variant. We don’t need to add another layer. It still only controls the visual appearance of the button’s base.
But it’s not the implementation that I would choose. I think it’s fine to have separate components for each variant, all within the same file:
Although we’re repeating some styles multiple times, the repeated code is close together, so I don’t consider it a big problem.
Even with different button sizes, I’d still prefer a over a .
There are two main reasons why I prefer separate components:
Not all components in a design will be reused. For instance, we might use a huge button with a shiny effect on the landing page that is never used again.
There’s often the tendency to change the button component and add attributes like and to it, even though it’s only used once in this exact combination. While it may seem like a small, quick change and we can reuse the existing button, we then make the decision to carry this burden around. Maintaining the button will be more difficult because we’ll always have to check that our special styling won’t break. At some point, the huge shiny button on the landing page is removed, but nobody removed the styles from the reusable button component.
If you encounter a button style that you haven’t seen before and it’s only used in one place, I recommend writing it inline instead of extending the existing button component. This tells all contributors that it is not meant to be reusable and only supports exactly what was needed.
We don’t need to think about the styles when the special button is disabled because it cannot be disabled. And when it’s removed, no remnants are left behind.
To wrap up some patterns that I use more often, you can add support for icons with just some CSS. This works well for left icons, right icons, and only icon buttons without text:
For more advanced cases—when you feel the need to add an attribute to the to control the icons—keep in mind that it’s better to separate the concerns and use composition. Don’t overload the button component with weird attributes again. A separate component that only handles the icon layout will be easier to maintain, and we can use it as an overlay on the base button.
Sadly, focus styles are often overlooked and lack sufficient emphasis. Often, they simply mirror the hover styles, but hover and focus are two very different things. A hover style alone isn’t enough to catch your eye when navigating with a keyboard.
Focus rings are an effective solution because they make the focus state highly visible. And because we separated the button into multiple layers, adding focus rings for all elements is quick and easy:
The focus ring is now a concern of the native button- and anchor element, so we can style it independently of the button component. It does not affect the button component at all, and the focus rings will always be present and consistent.
Of course, you could still define a focus style in the button component. But you don’t need to. It works great without it.
Adding a attribute to a component is one of the easiest ways to shoot yourself in the foot. It opens up the component too much, and quickly becomes hard to control. It invites contributors (and even future-you!) to mess with the internal styles from the outside, making it harder to change anything without breaking stuff.
Just don’t.
So, here we are! What started as “I just wanna write a short post about this pattern I’m using for buttons” quickly turned into a long article.
We talked a lot about buttons because, as we all know, they’re often the worst offenders in a codebase. At this point, “the button component” has basically become a meme. But somewhere along the way, it feels like we’ve started to accept that these components are inherently messy.
I asked ChatGPT to “write me a button component in React that implements all the necessary features that I will need”, and it shat out this:
This is why the button component is a meme. It’s a brittle, hard-to-maintain mess. But it also looks like something I’ve seen a lot in codebases—and, honestly, it’s something I’ve contributed to in the past. It shouldn’t be like this.
While I only talked about buttons in this article, the same patterns can and should be used for other UI components as well. Buttons are just the worst offenders, but they are not the only ones. I now use the same layered UI composition patterns for many more things, and I believe it is some of the best frontend code I have ever written.
Do I really need to write an introduction to button components? We all know them. Buttons are one of the most commonly used components in our user interfaces, and are also some of the messiest components, with crappy interfaces and complex implementations for something as simple as a freaking button, and they only get worse as your codebase ages.
In this post, I’ll briefly explain why button components are a classic problem in software engineering, how composition can solve this problem, and how I implement complex buttons with simple code these days.
Buttons seem so simple at first, but we need a lot from them: Primary buttons, secondary buttons, red buttons for destructive actions, bordered buttons, transparent buttons. In forms we need them to be an actual HTML <button> element, sometimes as type="submit" and sometimes as type="button". Sometimes they aren’t buttons, they are links and should be an <a> element, unless it’s client-side routing where we need to use a special <Link> component. Buttons come in different sizes. They should support icons on the left, icons on the right, or only an icon and no text—but then of course with a tooltip! They can be disabled, but sometimes they can look disabled even though they are not actually disabled. They can have a loading state, so it also needs to support that A͟N͟D͟ T͟H͟E͟N S͟O͟M͟E͟T͟I͟M͟E͟S T̷H̴E̶ ̶B̸U̶T̶T̴O̵N̸S̶ 𝙏𝙃𝙀𝙔 𝘼𝙍𝙀 𝘽𝙐𝙏𝙏𝙊𝙉𝙎 𝘛𝙃𝘼𝙏 𝘼𝙍𝙀𝙉’𝙏¿!? ✖︎ B̷U̸T̶T̷O̴N̸S̶ — THEY REFUSE to exist ╰┈➤ t͞h̸e͟y͞ ͞m͡e͜l͜t͡ ͟i͠n͜t̛o̕ ̴t̕h̨e͝ ͘c̛o͞d͡e͢, THEY TALK <Button prop="kill_me" /> THE PROPS ARE SCREAMING AT ME > Warning: prop 'scream' is not recognized on the DOM element. Did you mean 'screm'? onClick={() => END_MY_SUFFERING()} ⠀⛓ ᴀɴᴅ ʏᴇᴛ ɪ sᴛɪʟʟ ʜᴀᴠᴇ ᴛᴏ ᴅᴇᴀʟ ᴡɪᴛʜ as="a"
…
It’s a lot.
Buttons have to address many concerns. And our solution is really to try to squeeze all of that into a single component? Really?
You might say: “hang on, we don’t always use a single component!” And you’re right—sometimes you’ll see separate components, such as an <IconButton>. However, I’d argue that this often makes things worse. Because now you have to implement many of those complex concerns twice. While you remove a single concern from your big bad button component (for example, the concern that it only renders an icon and no text), your new special <IconButton> still looks like a button, can be disabled and hovered like a button, and needs to share most of the same styles. You’ll also need to implement the polymorphism twice to render different clickable HTML elements.
This problem sounds familiar! It seems like we’re dealing with separation of concerns, and simply refactoring it into an <IconButton> is the wrong separation. We don’t usually solve separation of concerns like this—we solve it with layering and composition!
The button component is nothing more than a classic software engineering problem that we usually solve with composition… yet I’ve rarely seen it solved with good composition!
Forget the idea of using a single button component. From now on, we’ll think of every button in our UI as multiple layered components.
The base layer of every button is an unstyled, native interactive element, such as a button element or an anchor, in which we’ll nest a component that looks like a button:
We immediately and completely remove polymorphism from our button components. That’s already a huge improvement. Polymorphic components are a big pain in the ass, and I try to avoid them wherever possible.
Note that we won’t encapsulate our layered button into a single button component again. This would just result in a messy polymorphic component with lots of props and complexity. We will always call all layers as we need them.
We can now achieve a lot with pure CSS. Adding hover and disabled states is straightforward and elegant:
It’s great that we can solve this simply by adding CSS, without having to change any existing code. There’s no need to concatenate classes dynamically, nor did we need to add an attribute to our button.
However, if necessary, we can also add a attribute to our button component. This would be useful if we needed a button that looks disabled but isn’t.
I’ve seen some really messy solutions to this problem, such as two separate and attributes, where it’s unclear in which combination they should be used. But with composition, we don’t need to worry about that! We can immediately see from the way it’s used that we have a clickable button that isn’t disabled, and it has a visual child that looks disabled.
I cannot stress enough how powerful this is. We’re eliminating ambiguity and complexity while writing code that documents itself.
I’ve heard engineers in the past who argued that this approach is bad because you end up repeating yourself more. For example, writing many times. It’s often seen as a violation of the “Don’t repeat yourself” (DRY) principle.
But DRY doesn’t mean that you’re never allowed to repeat yourself. It’s not about avoiding repetition at any cost. It’s about avoiding duplication of logic and knowledge.
In our case, the repetition is fine. It’s markup. The alternative, like , is the same repetition in a worse syntax.
Now that our base is in place, we can start adding individual components for each concern and layer them into our button.
For example, let’s say we want to add a loading state to our buttons. This means:
Here’s how we can implement this by separating each concern into its own layer:
With composition, we were able to solve it without a single change to the . That’s a good thing! It shows that our composition is working well.
Another positive sign is that all of our attributes are in the right place. We added a and attribute, both of which are obvious in the context of the component. This new component addresses one specific concern, and all its attributes are related to this concern.
To illustrate a poor design choice, let’s assume that we would add the loading state concern into the . This would result in something like this:
This is a whack component interface! The attributes and are related to each other, but is not. All attributes have to be optional because not every button has a loading state. However, setting makes no sense without . The relationship between the attributes is not immediately obvious. This interface requires documentation.
If you have a whack component interface like this and your attributes are a mess of unrelated concerns, it’s a clear sign that you need to separate these concerns into different layers.
Until now, we have only used a single . In reality we often have different button variants, such as a in a different color, or a transparent button, or a red button for destructive actions.
We could simply create an attribute for this, which would be an acceptable solution. But I’ll explain why we might not want this:
According to what I wrote in the previous section, an attribute is sufficient for the variant. We don’t need to add another layer. It still only controls the visual appearance of the button’s base.
But it’s not the implementation that I would choose. I think it’s fine to have separate components for each variant, all within the same file:
Although we’re repeating some styles multiple times, the repeated code is close together, so I don’t consider it a big problem.
Even with different button sizes, I’d still prefer a over a .
There are two main reasons why I prefer separate components:
Not all components in a design will be reused. For instance, we might use a huge button with a shiny effect on the landing page that is never used again.
There’s often the tendency to change the button component and add attributes like and to it, even though it’s only used once in this exact combination. While it may seem like a small, quick change and we can reuse the existing button, we then make the decision to carry this burden around. Maintaining the button will be more difficult because we’ll always have to check that our special styling won’t break. At some point, the huge shiny button on the landing page is removed, but nobody removed the styles from the reusable button component.
If you encounter a button style that you haven’t seen before and it’s only used in one place, I recommend writing it inline instead of extending the existing button component. This tells all contributors that it is not meant to be reusable and only supports exactly what was needed.
We don’t need to think about the styles when the special button is disabled because it cannot be disabled. And when it’s removed, no remnants are left behind.
To wrap up some patterns that I use more often, you can add support for icons with just some CSS. This works well for left icons, right icons, and only icon buttons without text:
For more advanced cases—when you feel the need to add an attribute to the to control the icons—keep in mind that it’s better to separate the concerns and use composition. Don’t overload the button component with weird attributes again. A separate component that only handles the icon layout will be easier to maintain, and we can use it as an overlay on the base button.
Sadly, focus styles are often overlooked and lack sufficient emphasis. Often, they simply mirror the hover styles, but hover and focus are two very different things. A hover style alone isn’t enough to catch your eye when navigating with a keyboard.
Focus rings are an effective solution because they make the focus state highly visible. And because we separated the button into multiple layers, adding focus rings for all elements is quick and easy:
The focus ring is now a concern of the native button- and anchor element, so we can style it independently of the button component. It does not affect the button component at all, and the focus rings will always be present and consistent.
Of course, you could still define a focus style in the button component. But you don’t need to. It works great without it.
Adding a attribute to a component is one of the easiest ways to shoot yourself in the foot. It opens up the component too much, and quickly becomes hard to control. It invites contributors (and even future-you!) to mess with the internal styles from the outside, making it harder to change anything without breaking stuff.
Just don’t.
So, here we are! What started as “I just wanna write a short post about this pattern I’m using for buttons” quickly turned into a long article.
We talked a lot about buttons because, as we all know, they’re often the worst offenders in a codebase. At this point, “the button component” has basically become a meme. But somewhere along the way, it feels like we’ve started to accept that these components are inherently messy.
I asked ChatGPT to “write me a button component in React that implements all the necessary features that I will need”, and it shat out this:
This is why the button component is a meme. It’s a brittle, hard-to-maintain mess. But it also looks like something I’ve seen a lot in codebases—and, honestly, it’s something I’ve contributed to in the past. It shouldn’t be like this.
While I only talked about buttons in this article, the same patterns can and should be used for other UI components as well. Buttons are just the worst offenders, but they are not the only ones. I now use the same layered UI composition patterns for many more things, and I believe it is some of the best frontend code I have ever written.
disabledvisuallyDisableddisabled<button><Button /></button><Button as="a" href="/pricing">PrimaryButtonlabelactiveLoadingLabelPrimaryButtonloadingLabelisLoadingdisabledloadingLabelisLoadingPrimaryButtonSecondaryButton<PrimaryButton size="sm"><Button variant="primary" size="sm">size="xxl"variant="shiny"PrimaryButtonclassNameclassNamesizedefaultsmlgicon<button disabled>
<PrimaryButton>Submit form</PrimaryButton>
</button>
function PrimaryButton(props) {
return (
<div
className="
p-4 bg-brand border border-brand-500 rounded
not-in-disabled:hover:bg-brand-600
in-disabled:bg-brand-300 in-disabled:text-gray
"
>
{props.children}
</div>
)
}
<button disabled>
<PrimaryButton
disabled
loadingLabel="Saving…"
isLoading={isSubmitting}
>
Save
</PrimaryButton>
</button>
<button>
<Button variant="primary">
Submit
</Button>
</button>
<button type="button">
<Button variant="secondary">
Reset
</Button>
</button>
function PrimaryButton(props) {
return (
<div
className="
p-4 bg-brand border border-brand-500 rounded
not-in-disabled:hover:bg-brand-600
in-disabled:bg-brand-300 in-disabled:text-gray
inline-flex flex-wrap
gap-1.5 items-center justify-center
*:[svg]:size-4 *:shrink-0
"
>
{props.children}
</div>
)
}
<button>
<PrimaryButton>
<FloppyDiskIcon />
Save
</PrimaryButton>
</button>
<a href="/page/2">
<PrimaryButton>
Next page
<ArrowRightIcon />
</PrimaryButton>
</a>
<Tooltip label="Edit">
<button disabled aria-label="Edit">
<PrimaryButton>
<PencilIcon />
</PrimaryButton>
</button>
</Tooltip>
<button>
<PrimaryButton>Submit form</PrimaryButton>
</button>
<a href="/pricing">
<PrimaryButton>View our pricing</PrimaryButton>
</a>
function PrimaryButton(props) {
return (
<div className="p-4 bg-brand border border-brand-500 rounded">
{props.children}
</div>
)
}
<button>
<PrimaryButton disabled>Submit form</PrimaryButton>
</button>
function PrimaryButton(props) {
return (
<div
data-disabled={props.disabled}
className="
p-4 bg-brand border border-brand-500 rounded
not-data-disabled:hover:bg-brand-600
data-disabled:bg-brand-300 data-disabled:text-gray
"
>
{props.children}
</div>
)
}
<a href="/signup">
<div className="p-8 bg-brand effect-shine text-xxl rounded">
Sign up
</div>
</a>
<button disabled>
<PrimaryButton disabled>
<LoadingLabel label="Saving…" active={isSubmitting}>
Save
</LoadingLabel>
</PrimaryButton>
</button>
function LoadingLabel(props) {
if (props.active) {
return (
<>
<SpinIcon />
<span role="status" aria-busy>
{props.label || 'Loading…'}
</span>
</>
)
}
return <>{props.children}</>
}
function PrimaryButton(props) {
return (
<div
data-disabled={props.disabled}
className="
p-4 bg-brand border border-brand-500 rounded
not-data-disabled:hover:bg-brand-600
data-disabled:bg-brand-300 data-disabled:text-gray
"
>
{props.children}
</div>
)
}
<button>
<PrimaryButton>
<IconLabel start={<FloppyDiskIcon />}>
Save
</IconLabel>
</PrimaryButton>
</button>
:focus-visible {
outline-color: var(--color-brand-300);
outline-offset: 4px;
outline-width: 2px;
border-radius: 2px;
}
function PrimaryButton(props) {
return (
<div
className="
p-4 bg-brand border border-brand-500 rounded
not-in-disabled:hover:bg-brand-600
in-disabled:bg-brand-300 in-disabled:text-gray
"
>
{props.children}
</div>
)
}
function SecondaryButton(props) {
return (
<div
className="
p-4 bg-gray border border-gray-500 rounded
not-in-disabled:hover:bg-gray-600
in-disabled:bg-gray-300 in-disabled:text-gray
"
>
{props.children}
</div>
)
}
function TransparentButton(props) {
return (
<div
className="
p-4 bg-transparent border border-transparent rounded
not-in-disabled:hover:bg-gray-100
in-disabled:bg-gray-200 in-disabled:text-gray
"
>
{props.children}
</div>
)
}
disabledvisuallyDisableddisabled<button><Button /></button><Button as="a" href="/pricing">PrimaryButtonlabelactiveLoadingLabelPrimaryButtonloadingLabelisLoadingdisabledloadingLabelisLoadingPrimaryButtonSecondaryButton<PrimaryButton size="sm"><Button variant="primary" size="sm">size="xxl"variant="shiny"PrimaryButtonclassNameclassNamesizedefaultsmlgicon<button disabled>
<PrimaryButton>Submit form</PrimaryButton>
</button>
function PrimaryButton(props) {
return (
<div
className="
p-4 bg-brand border border-brand-500 rounded
not-in-disabled:hover:bg-brand-600
in-disabled:bg-brand-300 in-disabled:text-gray
"
>
{props.children}
</div>
)
}
<button disabled>
<PrimaryButton
disabled
loadingLabel="Saving…"
isLoading={isSubmitting}
>
Save
</PrimaryButton>
</button>
<button>
<Button variant="primary">
Submit
</Button>
</button>
<button type="button">
<Button variant="secondary">
Reset
</Button>
</button>
function PrimaryButton(props) {
return (
<div
className="
p-4 bg-brand border border-brand-500 rounded
not-in-disabled:hover:bg-brand-600
in-disabled:bg-brand-300 in-disabled:text-gray
inline-flex flex-wrap
gap-1.5 items-center justify-center
*:[svg]:size-4 *:shrink-0
"
>
{props.children}
</div>
)
}
<button>
<PrimaryButton>
<FloppyDiskIcon />
Save
</PrimaryButton>
</button>
<a href="/page/2">
<PrimaryButton>
Next page
<ArrowRightIcon />
</PrimaryButton>
</a>
<Tooltip label="Edit">
<button disabled aria-label="Edit">
<PrimaryButton>
<PencilIcon />
</PrimaryButton>
</button>
</Tooltip>
<button>
<PrimaryButton>Submit form</PrimaryButton>
</button>
<a href="/pricing">
<PrimaryButton>View our pricing</PrimaryButton>
</a>
function PrimaryButton(props) {
return (
<div className="p-4 bg-brand border border-brand-500 rounded">
{props.children}
</div>
)
}
<button>
<PrimaryButton disabled>Submit form</PrimaryButton>
</button>
function PrimaryButton(props) {
return (
<div
data-disabled={props.disabled}
className="
p-4 bg-brand border border-brand-500 rounded
not-data-disabled:hover:bg-brand-600
data-disabled:bg-brand-300 data-disabled:text-gray
"
>
{props.children}
</div>
)
}
<a href="/signup">
<div className="p-8 bg-brand effect-shine text-xxl rounded">
Sign up
</div>
</a>
<button disabled>
<PrimaryButton disabled>
<LoadingLabel label="Saving…" active={isSubmitting}>
Save
</LoadingLabel>
</PrimaryButton>
</button>
function LoadingLabel(props) {
if (props.active) {
return (
<>
<SpinIcon />
<span role="status" aria-busy>
{props.label || 'Loading…'}
</span>
</>
)
}
return <>{props.children}</>
}
function PrimaryButton(props) {
return (
<div
data-disabled={props.disabled}
className="
p-4 bg-brand border border-brand-500 rounded
not-data-disabled:hover:bg-brand-600
data-disabled:bg-brand-300 data-disabled:text-gray
"
>
{props.children}
</div>
)
}
<button>
<PrimaryButton>
<IconLabel start={<FloppyDiskIcon />}>
Save
</IconLabel>
</PrimaryButton>
</button>
:focus-visible {
outline-color: var(--color-brand-300);
outline-offset: 4px;
outline-width: 2px;
border-radius: 2px;
}
function PrimaryButton(props) {
return (
<div
className="
p-4 bg-brand border border-brand-500 rounded
not-in-disabled:hover:bg-brand-600
in-disabled:bg-brand-300 in-disabled:text-gray
"
>
{props.children}
</div>
)
}
function SecondaryButton(props) {
return (
<div
className="
p-4 bg-gray border border-gray-500 rounded
not-in-disabled:hover:bg-gray-600
in-disabled:bg-gray-300 in-disabled:text-gray
"
>
{props.children}
</div>
)
}
function TransparentButton(props) {
return (
<div
className="
p-4 bg-transparent border border-transparent rounded
not-in-disabled:hover:bg-gray-100
in-disabled:bg-gray-200 in-disabled:text-gray
"
>
{props.children}
</div>
)
}
import React from "react";
import { cn } from "@/lib/utils";
export type ButtonProps = {
asChild?: boolean;
variant?: "default" | "destructive" | "outline" | "ghost" | "link";
size?: "default" | "sm" | "lg" | "icon";
loading?: boolean;
disabled?: boolean;
type?: "button" | "submit" | "reset";
children?: React.ReactNode;
className?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "default",
size = "default",
loading = false,
disabled = false,
type = "button",
className,
children,
...props
},
ref
) => {
return (
<button
ref={ref}
type={type}
disabled={disabled || loading}
className={cn(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none",
{
default: "bg-primary text-white hover:bg-primary/90",
destructive: "bg-red-500 text-white hover:bg-red-600",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
}[variant],
{
default: "h-10 px-4 py-2",
sm: "h-9 px-3",
lg: "h-11 px-8",
icon: "h-10 w-10",
}[size],
className
)}
{...props}
>
{loading ? <span className="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full" /> : null}
{children}
</button>
);
}
);
Button.displayName = "Button";
export { Button };
import React from "react";
import { cn } from "@/lib/utils";
export type ButtonProps = {
asChild?: boolean;
variant?: "default" | "destructive" | "outline" | "ghost" | "link";
size?: "default" | "sm" | "lg" | "icon";
loading?: boolean;
disabled?: boolean;
type?: "button" | "submit" | "reset";
children?: React.ReactNode;
className?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "default",
size = "default",
loading = false,
disabled = false,
type = "button",
className,
children,
...props
},
ref
) => {
return (
<button
ref={ref}
type={type}
disabled={disabled || loading}
className={cn(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none",
{
default: "bg-primary text-white hover:bg-primary/90",
destructive: "bg-red-500 text-white hover:bg-red-600",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
}[variant],
{
default: "h-10 px-4 py-2",
sm: "h-9 px-3",
lg: "h-11 px-8",
icon: "h-10 w-10",
}[size],
className
)}
{...props}
>
{loading ? <span className="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full" /> : null}
{children}
</button>
);
}
);
Button.displayName = "Button";
export { Button };