Skip to content
AgileLogics
6 min read

Styling Links and Interactive States

Prerequisites: The cascade, specificity, and inheritance

Pseudo-classes select elements in a particular state. They start with a single colon.

a { color: #0b57d0; }
a:visited { color: #6b3fa0; }
a:hover { color: #083b8a; text-decoration-thickness: 2px; }
a:focus-visible { outline: 3px solid #f5a623; outline-offset: 2px; }
a:active { color: #041d45; }
Pseudo-class When it matches
:link / :visited Unvisited / visited links (visited styling is limited to colors, for privacy)
:hover The pointer is over the element — doesn't exist on touch screens
:focus The element has keyboard focus (or was clicked)
:focus-visible Focused, and the browser thinks a focus ring should show (typically keyboard use)
:focus-within The element or anything inside it has focus
:active Being pressed
:disabled / :checked Form control states

Order matters

These rules all have the same specificity, so the later one wins when several states apply. Keep the classic order Link, Visited, Hover, Active (LoVe HAte) — otherwise :visited could override :hover.

Never remove the focus ring without replacing it

/* Wrong: keyboard users can no longer see where they are */
button:focus { outline: none; }

Instead, design a clear focus style and show it with :focus-visible:

:focus-visible {
  outline: 3px solid #f5a623;
  outline-offset: 2px;
}

Forcing states in a demo

Scripts can't trigger :hover, but :focus and :checked can be set and checked:

<style>
  input { outline: none; }
  input:focus { background: lightyellow; }
  label:has(input:checked) { font-weight: bold; }
  .field:focus-within { border-left: 4px solid teal; }
</style>
<div class="field"><input id="name" aria-label="Name"></div>
<label><input type="checkbox" id="agree"> I agree</label>
<script>
  const input = document.getElementById("name");
  const field = document.querySelector(".field");
  const label = document.querySelector("label");
  console.log("before:", getComputedStyle(input).backgroundColor, getComputedStyle(field).borderLeftWidth);
  input.focus();
  console.log("focused:", getComputedStyle(input).backgroundColor, getComputedStyle(field).borderLeftWidth);
  console.log("label weight:", getComputedStyle(label).fontWeight);
  document.getElementById("agree").click();
  console.log("label weight after checking:", getComputedStyle(label).fontWeight);
</script>
before: rgb(255, 255, 255) 0px
focused: rgb(255, 255, 224) 4px
label weight: 400
label weight after checking: 700

label:has(input:checked) selects a label containing a checked box — :has() is covered in Level 2.

Buttons that look like buttons

.button {
  display: inline-block;
  padding: 0.6rem 1.2rem;
  background: #0b57d0;
  color: white;
  border: 0;
  border-radius: 8px;
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}
.button:hover { background: #083b8a; }
.button:focus-visible { outline: 3px solid #f5a623; outline-offset: 2px; }
.button:disabled { opacity: 0.5; cursor: not-allowed; }

Keep a hover change and a focus style, and make links look distinguishable from surrounding text without relying on color alone — an underline is the clearest signal.

Practice

Style .card so its border turns blue when the pointer is over it, and when any link inside it has keyboard focus.

Show solution
.card {
  border: 1px solid #ddd;
}

.card:hover,
.card:focus-within {
  border-color: #0b57d0;
}