Complete CSS Course Module 12: Intermediate: CSS Components and UI Patterns

CSS Buttons: Styles, Sizes, Colors and Hover Effects

  • Beginner
  • Lesson 12.1
  • 6 min read
  • 51 of 77 in the course

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 em units for padding. Then the padding grows and shrinks with the font size, so a size class only needs to change font-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.

ClassWhat it changesGood for
.btn-sm / .btn-lgFont size, so the whole button scalesTables, hero sections
.btn-blockFull widthMobile forms
.btn-success, .btn-dangerBackground colorConfirm, delete
.btn-outlineBorder only, no fillSecond action
.btn-ghostText onlyThird action, toolbars
.btn-pillFully rounded endsFriendly, 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:hover a 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.

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 disabled attribute. That is why our disabled rule also matches [aria-disabled="true"]. For a real "not available" link, it is often better to remove the href or 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 .btn base class plus small modifier classes for size, color and shape.
  • Use em padding so size classes only need to change font-size, and a transparent border so all buttons share the same height.
  • Style every state: :hover, :active, :focus-visible and :disabled (or aria-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.