Every Effing things about Buttons
Buttons are those small clickable elements that you see on every website, which are the building blocks of user interactions on web. They may seem simple to build but are deceptively complex if you want to cover all the edge cases along with accessiblity. In this article, we will learn how to build a button that is accessible and just works.
Let's start with default button that comes with browser.
The default Button
<button>Default Button</button>
In HTML, we can use the <button>
tag to render out a button. Depending on the browser you are on, you will see the styles of default button differently.
We can reset the default styles with a bit of CSS:
button {
border-radius: 6px;
font-weight: 500;
border-width: 1px;
padding: 8px 16px;
border-style: solid;
border-color: transparent;
}
Do you know?
As soon you change background-color or border, The default styles for hover and active will stop working. This behavious is also different for different browsers.
Can we use a div
to create a button?
You can. but there are many functionalites that we get for free when we use a button
. For e.g:
- Buttons are focusable but divs aren't. We have to use
tab-index
attribute to make divs focusable. - The button onclick function can be triggered by pressing Enter or Space from keyboard..In divs, we will have to add the keydown event and check for space/enter event.
- The button announces that it is a button to assistive technology. For divs, we will have to add
role="button"
attribute.
Check out the article by Ben Myers for more detail around this topic.
In my opinion, it's okay to built buttons with div
as long as your button is accessible. The choice is yours.
Variants
On every product site, you will see atleast 2-3 kind of buttons with unique styles. These styles are called variants.
The most common pattern of naming button's variant is using names like primary
, secondary
and tertiary
.
Each variant has a special purpose:
- Primary - Primary buttons are used for the most important action on the page. For e.g: Add to cart button on product page.
- Secondary - Secondary buttons are alternative to primary actions. They have less prominence. For e.g: Cancel button on a form.
- Tertiary - Tertiary buttons are used for actions that are not important. For e.g: A link to terms and conditions.
Let's see how we can build these variants.
<button data-variant="primary">Primary</button>
<button data-variant="secondary">Secondary</button>
<button data-variant="tertiary">Tertiary</button>
So let's add our button's variant styles.
&, &[data-variant="primary"] {
background-color: #622ea5;
color: #fff;
}
&[data-variant="secondary"] {
background-color: transparent;
color: #622ea5;
border-color: #e1e1e1;
}
&[data-variant="tertiary"] {
background-color: transparent;
color: #622ea5;
border-color: transparent;
}
Note the line &, &[data-varaint-"primary"]
. We did this only for pimary variant. Can you guess the reason? The answer is that we want to apply the styles of primary when no variant is passed. So we are targetting both button
and button[data-variant="primary"]
with the same styles.
Classes vs data attributes
You might be thinking why we went with data-variant="filled"
instead of classes like button--primary
or btn primary
. The reason for it is CSS classes are considered harmful. Scaling with classes becomes messy quite easily.
Semantic Buttons
Buttons serve various purposes, and some of them involve potentially descructive actions, such as deleting a record. So it's better to use semantic variants like success
, danger
and warning
for such actions.
<button data-variant="success">Success</button>
<button data-variant="danger">Danger</button>
<button data-variant="warning">Warning</button>
&[data-variant="success"] {
background-color: #36b012;
color: #e6f6f1;
}
&[data-variant="danger"] {
background-color: #c32323;
color: #fef2f2;
}
&[data-variant="warning"] {
background-color: #e7b72b;
color: #664d03;
}
Cursor
You might have noticed that we are using cursor:pointer
( the hand-cusor ). The reason is hand cursor is meant for links. A well designed button does not need a hand cursor to tell user that it can be clicked. So it's not needed on buttons. But the choice is yours. You can use hand cursor if you want.
Buttons and Icons
Buttons with Icon
Buttons with icon are very common on internet. Let's be honest, Buttons without icons looks boring. So let's add the support for icon to our button. For the icon, We can use svg
or img
tag to render the icon. SVGs are better because they are scalable and we can change the color of svg using fill
property.
<button>
<svg data-icon aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2C12.5523 2 13 2.44772 13 3V6C13 6.55228 12.5523 7 12 7C11.4477 7 11 6.55228 11 6V3C11 2.44772 11.4477 2 12 2ZM12 17C12.5523 17 13 17.4477 13 18V21C13 21.5523 12.5523 22 12 22C11.4477 22 11 21.5523 11 21V18C11 17.4477 11.4477 17 12 17ZM22 12C22 12.5523 21.5523 13 21 13H18C17.4477 13 17 12.5523 17 12C17 11.4477 17.4477 11 18 11H21C21.5523 11 22 11.4477 22 12ZM7 12C7 12.5523 6.55228 13 6 13H3C2.44772 13 2 12.5523 2 12C2 11.4477 2.44772 11 3 11H6C6.55228 11 7 11.4477 7 12ZM19.0711 19.0711C18.6805 19.4616 18.0474 19.4616 17.6569 19.0711L15.5355 16.9497C15.145 16.5592 15.145 15.9261 15.5355 15.5355C15.9261 15.145 16.5592 15.145 16.9497 15.5355L19.0711 17.6569C19.4616 18.0474 19.4616 18.6805 19.0711
19.0711ZM8.46447 8.46447C8.07394 8.85499 7.44078 8.85499 7.05025 8.46447L4.92893 6.34315C4.53841 5.95262 4.53841 5.31946 4.92893 4.92893C5.31946 4.53841 5.95262 4.53841 6.34315 4.92893L8.46447 7.05025C8.85499 7.44078 8.85499 8.07394 8.46447 8.46447ZM4.92893 19.0711C4.53841 18.6805 4.53841 18.0474 4.92893 17.6569L7.05025 15.5355C7.44078 15.145 8.07394 15.145 8.46447 15.5355C8.85499 15.9261 8.85499 16.5592 8.46447 16.9497L6.34315 19.0711C5.95262 19.4616 5.31946 19.4616 4.92893 19.0711ZM15.5355 8.46447C15.145 8.07394 15.145 7.44078 15.5355 7.05025L17.6569 4.92893C18.0474 4.53841 18.6805 4.53841 19.0711 4.92893C19.4616 5.31946 19.4616 5.95262 19.0711 6.34315L16.9497 8.46447C16.5592 8.85499 15.9261 8.85499 15.5355 8.46447Z"></path></svg>
Button with Icon
</button>
We can add styles to our icon like this:
& [data-icon] {
width: 1.5em;
height: 1.5em;
& path {
fill: currentColor;
}
}
Few things to note:
- We used
em
units for icon's height and width. The reason is thatem
units are relative to the font-size of the element. So if we change the font-size of the button, the icon will also scale accordingly. - We added
aria-hidden="true"
on the svg icon. Reason being the icon in button are mainly for decorative purpose and we don't want screen readers to annouce it when there is already a text available.
Wait, Did you notice your button looks like bigger? It's because we used 1.5em on icon and it's scaling the button. We can fix this by limiting the height with block-size: 2rem
on button. Feel free to change this height of the button according to your needs.
button {
...
height: 36px;
...
}
Buttons with Only Icon
Buttons with only icon are also very common. We can use the same approach as above but we will need to add some extra styles to make it look good.
<button data-icon-button>
<svg data-icon aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2C12.5523 2 13 2.44772 13 3V6C13 6.55228 12.5523 7 12 7C11.4477 7 11 6.55228 11 6V3C11 2.44772 11.4477 2 12 2ZM12 17C12.5523 17 13 17.4477 13 18V21C13 21.5523 12.5523 22 12 22C11.4477 22 11 21.5523 11 21V18C11 17.4477 11.4477 17 12 17ZM22 12C22 12.5523 21.5523 13 21 13H18C17.4477 13 17 12.5523 17 12C17 11.4477 17.4477 11 18 11H21C21.5523 11 22 11.4477 22 12ZM7 12C7 12.5523 6.55228 13 6 13H3C2.44772 13 2 12.5523 2 12C2 11.4477 2.44772 11 3 11H6C6.55228 11 7 11.4477 7 12ZM19.0711 19.0711C18.6805 19.4616 18.0474 19.4616 17.6569 19.0711L15.5355 16.9497C15.145 16.5592 15.145 15.9261 15.5355 15.5355C15.9261 15.145 16.5592 15.145 16.9497 15.5355L19.0711 17.6569C19.4616 18.0474 19.4616 18.6805 19.0711 19.0711ZM8.46447 8.46447C8.07394 8.85499 7.44078 8.85499 7.05025 8.46447L4.92893 6.34315C4.53841 5.95262 4.53841 5.31946 4.92893 4.92893C5.31946 4.53841 5.95262 4.53841 6.34315 4.92893L8.46447 7.05025C8.85499 7.44078 8.85499 8.07394 8.46447 8.46447ZM4.92893 19.0711C4.53841 18.6805 4.53841 18.0474 4.92893 17.6569L7.05025 15.5355C7.44078 15.145 8.07394 15.145 8.46447 15.5355C8.85499 15.9261 8.85499 16.5592 8.46447 16.9497L6.34315 19.0711C5.95262 19.4616 5.31946 19.4616 4.92893 19.0711ZM15.5355 8.46447C15.145 8.07394 15.145 7.44078 15.5355 7.05025L17.6569 4.92893C18.0474 4.53841 18.6805 4.53841 19.0711 4.92893C19.4616 5.31946 19.4616 5.95262 19.0711 6.34315L16.9497 8.46447C16.5592 8.85499 15.9261 8.85499 15.5355 8.46447Z">
</path>
</svg>
</button>
We need to override the min-width. We need to add styles only when the button is an icon button. So the way we can tell that button is an icon button is passing a data attribute data-icon-button
. So our css would look like:
&[data-icon-button] {
min-inline-size: initial;
height: 36px;
width: 36px;
padding: 0;
}
Ok now our icon button looks good. But there is one accessiblity issue. There is no text in our icon button. So we need a text which can be annouced to assistive technologies. There are around 5+ ways to address this issue as suggested by Sara Soueidan:
- Using a visually hidden text
- Using a visually hidden text with
hidden
prop and referencing it witharia-describedby
attribute. - Using aria-label on the
<button>
element - Using aria-label on the
<svg>
icon element - Using aria-labelledby on the
<button>
element and referencing the<title>
element inside svg
We will go with #3 option as it seems like simplest one.
<button aria-label="Send message" data-icon-button>
<svg data-icon aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2C12.5523 2 13 2.44772 13 3V6C13 6.55228 12.5523 7 12 7C11.4477 7 11 6.55228 11 6V3C11 2.44772 11.4477 2 12 2ZM12 17C12.5523 17 13 17.4477 13 18V21C13 21.5523 12.5523 22 12 22C11.4477 22 11 21.5523 11 21V18C11 17.4477 11.4477 17 12 17ZM22 12C22 12.5523 21.5523 13 21 13H18C17.4477 13 17 12.5523 17 12C17 11.4477 17.4477 11 18 11H21C21.5523 11 22 11.4477 22 12ZM7 12C7 12.5523 6.55228 13 6 13H3C2.44772 13 2 12.5523 2 12C2 11.4477 2.44772 11 3 11H6C6.55228 11 7 11.4477 7 12ZM19.0711 19.0711C18.6805 19.4616 18.0474 19.4616 17.6569 19.0711L15.5355 16.9497C15.145 16.5592 15.145 15.9261 15.5355 15.5355C15.9261 15.145 16.5592 15.145 16.9497 15.5355L19.0711 17.6569C19.4616 18.0474 19.4616 18.6805 19.0711 19.0711ZM8.46447 8.46447C8.07394 8.85499 7.44078 8.85499 7.05025 8.46447L4.92893 6.34315C4.53841 5.95262 4.53841 5.31946 4.92893 4.92893C5.31946 4.53841 5.95262 4.53841 6.34315 4.92893L8.46447 7.05025C8.85499 7.44078 8.85499 8.07394 8.46447 8.46447ZM4.92893 19.0711C4.53841 18.6805 4.53841 18.0474 4.92893 17.6569L7.05025 15.5355C7.44078 15.145 8.07394 15.145 8.46447 15.5355C8.85499 15.9261 8.85499 16.5592 8.46447 16.9497L6.34315 19.0711C5.95262 19.4616 5.31946 19.4616 4.92893 19.0711ZM15.5355 8.46447C15.145 8.07394 15.145 7.44078 15.5355 7.05025L17.6569 4.92893C18.0474 4.53841 18.6805 4.53841 19.0711 4.92893C19.4616 5.31946 19.4616 5.95262 19.0711 6.34315L16.9497 8.46447C16.5592 8.85499 15.9261 8.85499 15.5355 8.46447Z">
</path>
</svg>
</button>
Hover and Active State
When we modify the border-style or background-color, we lose the hover and active styles, requiring us to redefine these styles to maintain the buttons's interactive appearance. We can subtly alter the background, or we can translate the button slightly upon hover or when it's pressed. The key consideration is ensuring that users can distinguish between the normal, hover, and active states. In this article, we will focus on altering the colors for the hover and pressed states. Additionally, for the pressed state, we will apply a 1px translation to the button to create a tactile "pressed" sensation.
&, &[data-variant="primary"] {
background-color: #1d83f7;
color: #fff;
&:hover {
background-color: #358df1
}
&:active {
background-color: #2371c9
}
}
Similarly we can add the styles for other variants too.
Focus State
<button>
element comes with some focus styles that is dictated by browsers. So if you focus on any of the previous button above, you will see a some focus styles depending on the browser you are using. We can update the focus styles to match our design.
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid #1d83f7;
outline-offset: 2px;
}
:focus-vislble vs :focus
Note how we used :focus-visible
pseudo selector instead of :focus
. The reason is that for buttons, :focus-visible
gets active when button receives focus using keyboard whereas :focus
gets active when button receives focus even on clicking.
Read more about :focus-visible
Disabled State
Disabled state is the most misundestood state in buttons. What people generally do to mark a button as disabled is pass the disabled
prop.
<button disabled>Disabled Button</button>
It does two things for us:
- It stops click/press event from being triggered.
- It prevents the button from being focused.
Note that using :disabled
attribute won't stop :hover
or :active
styles. They would work as it is.
Using disabled
attribute may seem useful but there are one major problem with it. Special-abled people who use keyboard to navigate the websites will not be able to navigate to the button. Worse, they might not even know there exists a button that is disabled.
So we have to make sure that we avoid using disabled
prop in our button.
But wait, how do we tell our users that button is disabled without using disabled
attribute? Well, There are two solutions to the problem as suggested by Sandrina Pereira.
Solution #1 Use tooltips
In this approach, we avoid using disabled
prop and style our buttons targetting with [aria-disabled]
selector to like a disabled button.
&[aria-disabled] {
opacity: 0.6;
cursor: not-allowed;
}
Note that aria-disabled
does not actually do anything except helping us annoucing to assitive technologies that button is disabled. We can still click on button and trigger our onclick method. So we will need to tweak our onclick function so that nothing happens on click when button is disabled.
const onClick = () => {
const isDisabled = elButtonSubmit.getAttribute('aria-disabled') === 'true';
if (isDisabled) return;
// Do some async work here...
}
There is some UX problem though. But we are not telling our users why the button is disabled. We can't simply leave our users guessing right? So to solve this, We can use tooltip on our button to tell users why the button is disabled. When using tooltips on button, we need to make sure we are using proper aria attributes to enhance the experience for assistive technologies.
Our hover and active styles are still working. They shouldn't work on disabled buttons. So we will need to tweak our styles a bit.
button {
&, &[data-variant="primary"] {
...
&:not([aria-disabled]):hover {
...
}
&:not([aria-disabled]):active {
...
}
}
}
Why can't we just use pointer-events: none
?
pointer-events:none
has the problem that disabled
has. It stops all click events along with focus events. So users can't navigate to the button using keyboard. Special-abled people won't notice that the button exists if we use pointer-events:none
( same problem we had with disabled attribute ). So avoid using pointer-events: none
too.
Solution #2 Don't use disabled buttons at all
Solution #1 works great but our users will be uncertain about button why its disabled until they hover. Also, we altered styles and made it little dim which might not look good visually.
Another way to solve issues with disabled buttons is avoid using them at all. Instead of disabling the button, we allows click and show a feedback on click.
Loading State
Buttons can be used to do some async tasks. In such cases, we need to show some feedback to user that something is happening.
There are few things we need to do when the button goes into loading state.
Firstly, we need a to change structure of our button. We need a span that will contain our loader icon.
<button>
<span data-content>Add to Cart</span>
<span data-loader aria-hidden="true">
<svg data-icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C12.5523 2 13 2.44772 13 3V6C13 6.55228 12.5523 7 12 7C11.4477 7 11 6.55228 11 6V3C11 2.44772 11.4477 2 12 2ZM12 17C12.5523 17 13 17.4477 13 18V21C13 21.5523 12.5523 22 12 22C11.4477 22 11 21.5523 11 21V18C11 17.4477 11.4477 17 12 17ZM22 12C22 12.5523 21.5523 13 21 13H18C17.4477 13 17 12.5523 17 12C17 11.4477 17.4477 11 18 11H21C21.5523 11 22 11.4477 22 12ZM7 12C7 12.5523 6.55228 13 6 13H3C2.44772 13 2 12.5523 2 12C2 11.4477 2.44772 11 3 11H6C6.55228 11 7 11.4477 7 12ZM19.0711 19.0711C18.6805 19.4616 18.0474 19.4616 17.6569 19.0711L15.5355 16.9497C15.145 16.5592 15.145 15.9261 15.5355 15.5355C15.9261 15.145 16.5592 15.145 16.9497 15.5355L19.0711 17.6569C19.4616 18.0474 19.4616 18.6805 19.0711 19.0711ZM8.46447 8.46447C8.07394 8.85499 7.44078 8.85499 7.05025 8.46447L4.92893 6.34315C4.53841 5.95262 4.53841 5.31946 4.92893 4.92893C5.31946 4.53841 5.95262 4.53841 6.34315 4.92893L8.46447 7.05025C8.85499 7.44078 8.85499 8.07394 8.46447 8.46447ZM4.92893 19.0711C4.53841 18.6805 4.53841 18.0474 4.92893 17.6569L7.05025 15.5355C7.44078 15.145 8.07394 15.145 8.46447 15.5355C8.85499 15.9261 8.85499 16.5592 8.46447 16.9497L6.34315 19.0711C5.95262 19.4616 5.31946 19.4616 4.92893 19.0711ZM15.5355 8.46447C15.145 8.07394 15.145 7.44078 15.5355 7.05025L17.6569 4.92893C18.0474 4.53841 18.6805 4.53841 19.0711 4.92893C19.4616 5.31946 19.4616 5.95262 19.0711 6.34315L16.9497 8.46447C16.5592 8.85499 15.9261 8.85499 15.5355 8.46447Z"></path></svg>
</span>
</button>
As we learnt in "Button with only Icon" section, screen readers can't tell from the icon what does it mean, We will use aria-hidden
attribute to hide the loader icon from screen readers.
So by default this loader will be hidden.
& [data-loader] {
display: none;
}
& [data-loader] [data-icon] {
animation: spin 1.5s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
And, When the loader goes into loading state, we will show the loader and hide the content.
Note we need something that can represent our loading state so that we can target that state and use in CSS to show/hide the loader. It can a class like .loading
or .is-loading
or a data attribute like [data-loading]
. We will go with data attribute.
&[data-loading] {
& [data-content] {
visibility: hidden;
}
& [data-loader] {
display: flex;
position: absolute;
inset: 0;
justify-content: center;
align-items: center;
border-radius: inherit;
}
}
display:none vs visibility: hidden
You might be thinking, why not simply do display: none
on content. Well, we can't do that because we need to keep the button's size same when it goes into loading state.
So we will use visibility: hidden
on content and position: absolute
on loader to keep the button's size same.
So on click, we toggle our data-loading
data-attribute.
const onClick = (e) => {
const currentButton = e.currentTarget;
const isLoading = currentButton.hasAttribute('data-loading');
if (isLoading) return;
elButtonSubmit.setAttribute('data-loading');
// Note: We are using setTimeout to simulate the async work
setTimeout() => {
elButtonSubmit.removeAttribute('data-loading');
}, 3000);
}
There is still one major problem we didn't tackle. Our button is not accessible for loading state.
Since we used aria-hidden
for our [data-loader]
, our button is not annoucing to the assistive technologies that it is doing some aync task. For this, we will use a visually hidden text to annouce to assistive technologies that button is in loading state.
<button>
<span data-content aria-hidden="true">Add to Cart</span>
<span data-loader aria-hidden="true">
<svg data-icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C12.5523 2 13 2.44772 13 3V6C13 6.55228 12.5523 7 12 7C11.4477 7 11 6.55228 11 6V3C11 2.44772 11.4477 2 12 2ZM12 17C12.5523 17 13 17.4477 13 18V21C13 21.5523 12.5523 22 12 22C11.4477 22 11 21.5523 11 21V18C11 17.4477 11.4477 17 12 17ZM22 12C22 12.5523 21.5523 13 21 13H18C17.4477 13 17 12.5523 17 12C17 11.4477 17.4477 11 18 11H21C21.5523 11 22 11.4477 22 12ZM7 12C7 12.5523 6.55228 13 6 13H3C2.44772 13 2 12.5523 2 12C2 11.4477 2.44772 11 3 11H6C6.55228 11 7 11.4477 7 12ZM19.0711 19.0711C18.6805 19.4616 18.0474 19.4616 17.6569 19.0711L15.5355 16.9497C15.145 16.5592 15.145 15.9261 15.5355 15.5355C15.9261 15.145 16.5592 15.145 16.9497 15.5355L19.0711 17.6569C19.4616 18.0474 19.4616 18.6805 19.0711 19.0711ZM8.46447 8.46447C8.07394 8.85499 7.44078 8.85499 7.05025 8.46447L4.92893 6.34315C4.53841 5.95262 4.53841 5.31946 4.92893 4.92893C5.31946 4.53841 5.95262 4.53841 6.34315 4.92893L8.46447 7.05025C8.85499 7.44078 8.85499 8.07394 8.46447 8.46447ZM4.92893 19.0711C4.53841 18.6805 4.53841 18.0474 4.92893 17.6569L7.05025 15.5355C7.44078 15.145 8.07394 15.145 8.46447 15.5355C8.85499 15.9261 8.85499 16.5592 8.46447 16.9497L6.34315 19.0711C5.95262 19.4616 5.31946 19.4616 4.92893 19.0711ZM15.5355 8.46447C15.145 8.07394 15.145 7.44078 15.5355 7.05025L17.6569 4.92893C18.0474 4.53841 18.6805 4.53841 19.0711 4.92893C19.4616 5.31946 19.4616 5.95262 19.0711 6.34315L16.9497 8.46447C16.5592 8.85499 15.9261 8.85499 15.5355 8.46447Z"></path></svg>
</span>
<span class="visually-hidden" aria-live="assertive"></span>
</button>
The way aria-live works is as soon as the content of live region ( div with aria-live
) changes, the screen readers will start annoucing. So we will need to change the content with javascript.
const onClick = (e) => {
const currentButton = e.currentTarget;
const isLoading = currentButton.hasAttribute('data-loading');
if (isLoading) return;
currentButton.setAttribute('data-loading', "");
currentButton.querySelector('[aria-live]').textContent = 'Wait, Adding to cart.';
// Note: We are using setTimeout to simulate the async work
setTimeout(() => {
currentButton.removeAttribute('data-loading');
currentButton.querySelector('[aria-live]').textContent = '';
}, 3000);
}
document.querySelector('button').addEventListener('click', onClick);
Ok now, If you use voiceover or ndva, you will see that when our button goes in loading state, it annouces the state. But what about post loading state? We should also announce that async work is done. This is mostly handled by another live region that can render a toast or simply status message outside of button.
Note in the above example, we didn't address what happens after async task is done and how to annouce it to the user. We will leave that for you to figure out :D ( You will need toast with aria-live attribute )
Bonus
There are still few things we can improve. All these improvements are coming from the eyes of a design system developer. So feel free to ignore them if you are not building a design system.
- Use spacing tokens instead of hardcoding the padding. Every design system has a spacing scale. Instead of hardcoding values in pixel in our button, we can use spacing tokens. This will help us in maintaining the consistency across the design system. Note your design system can sizing and spacing tokens separately. So use them accordingly.
:root {
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
...
--spacing-10: 2.5rem;
}
button {
...
padding: 0 var(--spacing-2);
height: var(--spacing-10);
...
}
- Use color tokens instead of hardcoding the colors. For same reason as above, we can use color tokens instead of hardcoding the colors.
:root {
...
--color-bg-accent: #1d83f7;
--color-bg-accent-hover: #358df1;
--color-bg-accent-active: #2371c9;
--color-fg-onaccent: #fff;
...
}
button {
...
&, &[data-variant="primary"] {
background-color: var(--color-bg-accent);
color: var(--color-fg-onaccent);
&:hover {
background-color: var(--color-bg-accent-hover);
}
&:active {
background-color: var(--color-bg-accent-active);
}
}
}
This not only helps in maintaining consistency but also helps in theming the design system. Using the colors in our button, we can easily create dark mode or another theme for our design system.
body[data-theme="dark"] {
--color-bg-accent: #1d83f7;
--color-bg-accent-hover: #358df1;
--color-bg-accent-active: #2371c9;
--color-fg-onaccent: #fff;
}
body[data-theme="high-contrast"] {
--color-bg-accent: #1d83f7;
--color-bg-accent-hover: #358df1;
--color-bg-accent-active: #2371c9;
--color-fg-onaccent: #fff;
}
- Instead of using properties like height or width, use block-size and inline-size. block-size and inline-size are logical properties. They come in handy when we are building a design system that supports RTL languages or different writing flows ( like vertical writing flow ).
button {
...
min-block-size: var(--spacing-10);
inline-size: var(--spacing-10);
...
}