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;
}