In short
CSS buttons guide: reset the default look, add sizes, colors, outline and pill styles, icon buttons, button groups, hover and focus states, and a loading state.
CSS buttons guide: reset the default look, add sizes, colors, outline and pill styles, icon buttons, button groups, hover and focus states, and a loading state.
How to reset the default button style
You met a simple button with hover, focus and disabled states in the Styling Forms and Buttons lesson. In this lesson we go further and build a small button system: one base class plus extra classes for size, color and shape. It works like LEGO: one base brick, and you snap extra pieces on top.
Every browser draws <button> a little differently. So we start with a base class that removes the browser look and sets the parts every button shares.
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
padding: 0.6em 1.2em;
border: 2px solid transparent;
border-radius: 8px;
background: #2563eb;
color: white;
font: inherit;
font-weight: 600;
line-height: 1.2;
text-decoration: none;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s, transform 0.1s;
}
<button class="btn" type="button">Save changes</button>
You see a blue button with white bold text and soft corners. display: inline-flex lets us put an icon and text side by side later, and gap puts space between them. The transparent border is a small trick: outline buttons will have a visible border, and because every button already has a 2px border, all buttons stay exactly the same height.
Tip: Use
emunits for padding. Then the padding grows and shrinks with the font size, so a size class only needs to changefont-size.
Button sizes, colors and shapes
Now we add modifier classes. A modifier is a small extra class that changes one thing, like size or color. You write two classes in the HTML: class="btn btn-lg".
/* Sizes */
.btn-sm { font-size: 0.875rem; }
.btn-lg { font-size: 1.25rem; }
.btn-block { display: flex; width: 100%; }
/* Colors */
.btn-success { background: #16a34a; }
.btn-danger { background: #dc2626; }
.btn-dark { background: #1f2937; }
/* Outline and ghost */
.btn-outline {
background: transparent;
border-color: #2563eb;
color: #2563eb;
}
.btn-ghost {
background: transparent;
color: #2563eb;
}
/* Shapes */
.btn-pill { border-radius: 999px; }
.btn-square { border-radius: 0; }
<button class="btn btn-sm" type="button">Small</button>
<button class="btn" type="button">Normal</button>
<button class="btn btn-lg btn-pill" type="button">Large pill</button>
<button class="btn btn-danger" type="button">Delete</button>
<button class="btn btn-outline" type="button">Cancel</button>
<button class="btn btn-ghost" type="button">Skip</button>
You see a row of buttons. The small and large ones keep the same shape because the padding is in em. The pill button has fully round ends, because a huge border-radius like 999px always makes a perfect pill. The outline button has a colored border and no fill, and the ghost button has no border and no fill at all, only colored text. Outline and ghost buttons are great for less important actions, like "Cancel", next to a strong main button.
| Class | What it changes | Good for |
|---|---|---|
.btn-sm / .btn-lg | Font size, so the whole button scales | Tables, hero sections |
.btn-block | Full width | Mobile forms |
.btn-success, .btn-danger | Background color | Confirm, delete |
.btn-outline | Border only, no fill | Second action |
.btn-ghost | Text only | Third action, toolbars |
.btn-pill | Fully rounded ends | Friendly, modern look |
Hover, active, focus-visible and disabled states
A button talks to the user through its states. Hover says "you can click me", active says "you are pressing me", focus says "the keyboard is here", and disabled says "not right now".
.btn:hover { background: #1d4ed8; }
.btn-success:hover { background: #15803d; }
.btn-danger:hover { background: #b91c1c; }
.btn-outline:hover,
.btn-ghost:hover { background: #eff6ff; }
.btn:active { transform: translateY(1px) scale(0.98); }
.btn:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
Hover each button: the colored ones get darker, the outline and ghost ones get a light blue background. When you press, the button shrinks a tiny bit, like a real button pushed into the page. Press Tab and an orange ring appears only for keyboard users, thanks to :focus-visible. Disabled buttons fade out and stop reacting.
Common mistake: Giving
.btn:hovera color but forgetting each color variant. Then a red Delete button turns blue on hover. Give every color class its own hover color, or use a CSS variable for the color (you will learn variables in the Advanced module).
Icon buttons, button groups and a loading state
An icon button shows only an icon, like a close "X" or a heart. Because there is no visible text, you must add aria-label so screen readers can say what the button does.
<button class="btn btn-icon" type="button" aria-label="Close">
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</button>
<div class="btn-group" role="group" aria-label="Text alignment">
<button class="btn btn-outline" type="button">Left</button>
<button class="btn btn-outline" type="button">Center</button>
<button class="btn btn-outline" type="button">Right</button>
</div>
<button class="btn is-loading" type="button" aria-busy="true">Sending</button>
.btn-icon {
padding: 0.5em;
border-radius: 50%;
}
.btn-group {
display: inline-flex;
}
.btn-group .btn {
border-radius: 0;
}
.btn-group .btn + .btn {
margin-left: -2px; /* borders overlap instead of doubling */
}
.btn-group .btn:first-child { border-radius: 8px 0 0 8px; }
.btn-group .btn:last-child { border-radius: 0 8px 8px 0; }
.is-loading {
pointer-events: none;
opacity: 0.8;
}
.is-loading::after {
content: "";
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
The icon button is a blue circle with a white X. The SVG uses currentColor, so the icon always matches the text color. The button group shows three joined buttons with one shared border and rounded corners only at the outer ends. The loading button shows "Sending" with a small spinning circle next to it. The circle is a border with one side transparent, rotated forever. You will learn @keyframes properly in the Animations lesson.
How to style a link as a button
Sometimes a link should look like a button, for example a "Sign up" link in a hero section. Our .btn class already has text-decoration: none and display: inline-flex, so it works on <a> too.
<a class="btn btn-lg btn-pill" href="/signup">Start free trial</a>
<button class="btn btn-outline btn-lg btn-pill" type="submit">Send form</button>
Both look the same: large pill buttons. The rule for choosing the tag is simple. If it goes to another page, use <a href>. If it does something on this page (submit, open, delete), use <button>. Screen readers and keyboards treat them differently, so the right tag matters even when the look is the same.
Note: A link has no
disabledattribute. That is why our disabled rule also matches[aria-disabled="true"]. For a real "not available" link, it is often better to remove thehrefor not show the link at all.
Practice exercise
Build a small "pricing card" footer with three buttons: a large green pill "Buy now" button, an outline "Compare plans" link styled as a button, and a round icon button with a heart. Then add a "Processing" loading version of the Buy button.
Hint: start from the .btn base class, then combine modifiers like class="btn btn-lg btn-pill btn-success". Remember aria-label="Add to favorites" on the heart button.
Key takeaways
- Build buttons from one
.btnbase class plus small modifier classes for size, color and shape. - Use
empadding so size classes only need to changefont-size, and a transparent border so all buttons share the same height. - Style every state:
:hover,:active,:focus-visibleand:disabled(oraria-disabled). - Icon-only buttons need
aria-label; loading buttons can show a CSS spinner with::after. - Use
<a>to go somewhere and<button>to do something, even when they look the same.