Styling Links
Links are the most clicked thing on the web. The browser's default style, blue or purple text with an underline, is ugly to some designers, but it has one great quality: everybody knows it is a link. When you restyle links, your job is to match your design and keep that instant recognition, for mouse, touch and keyboard users alike.
In this lesson you will style every link state in the right order, give keyboard users a clear focus ring, refine underlines, mark external and download links automatically, and make links that look like buttons.
The link states
A link can be in several states at once, and each has a pseudo-class (you met these in Combinators & Pseudo-classes):
| Pseudo-class | Matches |
|---|---|
:link | A link the user has not visited yet |
:visited | A link to a page already in the browser history |
:any-link | Both of the above: any <a> that has an href |
:focus-visible | The focused link, when a visible ring is helpful (mainly keyboard use) |
:hover | The mouse pointer is over the link |
:active | The link is being pressed or clicked |
An <a> element without an href attribute is not a link, so :link, :visited and :any-link do not match it.
Why the order matters: LVHA
All these pseudo-classes have the same specificity. When two of them match at once, such as a visited link that is also hovered, the rule written later in the file wins. So write them in this order:
:linkand:visited(the resting states):focus-visibleand:hover(the user is pointing at it):active(the user is pressing it right now)
The memory trick is LoVe HAte: Link, Visited, Hover, Active. If you put :hover before :visited, visited links never show the hover style, because the :visited rule comes later and overrides it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Link states</title>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; color: #1f2937; line-height: 1.6; max-width: 38rem; }
a:link { color: oklch(45% 0.17 255); }
a:visited { color: oklch(42% 0.16 320); }
a:focus-visible {
outline: 3px solid oklch(65% 0.18 60);
outline-offset: 2px;
border-radius: 2px;
}
a:hover { color: oklch(35% 0.17 255); text-decoration-thickness: 3px; }
a:active { color: oklch(50% 0.2 25); }
a {
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
}
</style>
</head>
<body>
<p>Farhan is planning a weekend in Jaipur. He has opened the <a href="#forts">guide to the city forts</a>, saved the <a href="#metro">metro route map</a> and is comparing <a href="#hotels">hotels near Hawa Mahal</a> under ₹3,000 a night.</p>
<p><a href="#top">Back to top</a></p>
</body>
</html>Press Tab in the result to move between the links: each focused link gets an orange ring. Point at a link to see the darker colour and thicker underline, and hold the mouse button down to see the red :active colour.
For privacy, browsers let :visited change only colours, such as color, background-color and text-decoration-color. Anything else is ignored, so a website cannot measure your history by checking which links got bigger.
Focus rings for keyboard users
Many people use the keyboard instead of a mouse. The focus ring is the only way they can see which link is selected. :focus-visible shows a ring when it helps, mainly for keyboard use, and not after every mouse click.
- Use
outline(notborder) for the ring: it does not change the link's size, so nothing on the page moves. outline-offsetadds a small gap so the ring does not cover the letters.- Make the ring colour contrast with the page background.
Better underlines
Links inside running text should stay underlined. Many people cannot tell colours apart, so colour alone is not enough to show that text is clickable. Instead of removing the underline, make it look better:
.article a {
text-decoration-line: underline;
text-decoration-thickness: 0.08em; /* scales with the font size */
text-underline-offset: 0.18em; /* a little gap below the letters */
text-decoration-color: color-mix(in oklch, currentColor 45%, transparent);
transition: text-decoration-color 0.2s;
}
.article a:hover {
text-decoration-color: currentColor; /* the full-strength line on hover */
}text-decoration-thicknesssets the line's thickness.emvalues grow with the text;from-fontuses the thickness suggested by the font.text-underline-offsetmoves the line down, away from the letters.text-decoration-colorcan differ from the text colour. Here it is a softer version of the link colour, made withcolor-mix().text-decoration-skip-ink: autois the default in modern browsers: the line breaks around letters that hang below it, such as g, j, p and y.
Navigation menus, cards and buttons are different. Their position and design already say "click me", so they may drop the underline. See Navigation Bars.
Marking external and download links
Attribute selectors can find special links for you, so authors never have to add classes by hand:
| Selector | Matches links whose href... |
|---|---|
a[href^="https://"] | starts with https:// |
a[href$=".pdf"] | ends with .pdf |
a[href^="mailto:"] | is an email link |
a[href^="tel:"] | is a phone link |
a[download] | has the download attribute |
a[target="_blank"] | opens in a new tab or window |
Combine them with :not() to skip links to your own website, and use ::after to add a small marker. The content property accepts alternative text after a slash: content: "↗" / "" shows the arrow but tells screen readers to say nothing for it. Alternative text for content works in current versions of all major browsers.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Link markers and button links</title>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; color: #1f2937; line-height: 1.6; max-width: 38rem; }
a { color: oklch(45% 0.17 255); text-underline-offset: 0.18em; }
a:focus-visible { outline: 3px solid oklch(65% 0.18 60); outline-offset: 2px; }
/* external links, except links to our own site */
a[href^="https://"]:not([href^="https://kochi-ferry.example"])::after {
content: "↗" / "";
margin-inline-start: 0.15em;
font-size: 0.85em;
}
/* PDF files */
a[href$=".pdf"]::after {
content: " (PDF)";
font-size: 0.85em;
color: #6b7280;
}
.button-link {
display: inline-block;
padding: 0.7rem 1.3rem;
border-radius: 8px;
background: oklch(45% 0.17 255);
color: white;
font-weight: 600;
text-decoration: none;
}
.button-link:hover { background: oklch(38% 0.17 255); }
.button-link:active { translate: 0 1px; }
.visually-hidden {
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
</style>
</head>
<body>
<p>Check the <a href="https://kochi-ferry.example/timings">ferry timings</a>, download the <a href="/files/fares-2026.pdf">fare chart</a>, or read about the backwaters on the <a href="https://kerala-tourism.example/backwaters" target="_blank">tourism website<span class="visually-hidden"> (opens in a new tab)</span></a>.</p>
<p><a class="button-link" href="#book">Book a ferry ticket</a></p>
</body>
</html>The first link points at "our own" site, so it gets no arrow. The tourism link gets an arrow, and the PDF link gets a label. Important information, such as "opens in a new tab", is written in the HTML inside a visually hidden span (see Display & Visibility), so every screen reader announces it even though sighted users only see the arrow.
Links that look like buttons
"Book a ferry ticket" takes the user to another page, so it must stay an <a> element: links go somewhere, buttons do something (submit a form, open a menu). It can still look like a button:
display: inline-block(orinline-flex) so padding pushes the surrounding lines apart properly;- padding, a background colour and
border-radius; text-decoration: none, because the button shape already shows it is clickable;- clear
:hover,:activeand:focus-visiblestyles; - a comfortable size for fingers: at least 24 by 24 CSS pixels, and around 44 pixels high is easier on phones.
Never use <a href="#"> with JavaScript for an action such as "Add to cart". It breaks keyboard and screen reader behaviour; use a real <button>. Full button styling is covered in Buttons.
Common mistakes
- Wrong state order.
:hoverwritten before:visitednever shows on visited links. Follow LVHA. - Removing the focus outline. Always give keyboard users a visible
:focus-visiblestyle. - Removing underlines from links in paragraphs. Colour alone is not enough; refine the underline instead.
- Styling
:visitedwith sizes or fonts. Browsers ignore everything except colours there. - Markers that screen readers read badly. Hide decorative symbols with
content: "↗" / "", and put real information in the HTML. - Using a link for an action. If it does not go to a URL, it should be a
<button>.
Key points
- Write link states in the order
:link,:visited,:focus-visible/:hover,:active(LoVe HAte). :any-linkmatches every<a>with anhref;:visitedcan only change colours.- Use
:focus-visiblewithoutlineandoutline-offsetfor keyboard focus rings. - Keep underlines on links in text and refine them with
text-decoration-thickness,text-underline-offsetandtext-decoration-color. - Attribute selectors such as
[href$=".pdf"]and[href^="https://"]add markers automatically. - A link that navigates can look like a button, but actions belong in a real
<button>.
Exercise
Style the links of a Lucknow school's notice page:
- links in paragraphs keep a soft underline that becomes solid on hover;
- every PDF link gets a " (PDF)" label after it;
- links to other websites get an arrow that screen readers skip;
- add a "Pay school fees" link styled as a green button with a focus ring;
- write the states in the correct order.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>School notices</title>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; line-height: 1.6; max-width: 40rem; }
a:link { color: #1d4ed8; }
a:visited { color: #6d28d9; }
a:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }
a:hover { text-decoration-color: currentColor; }
a:active { color: #b91c1c; }
p a {
text-decoration-thickness: 0.08em;
text-underline-offset: 0.18em;
text-decoration-color: color-mix(in oklch, currentColor 40%, transparent);
}
a[href$=".pdf"]::after { content: " (PDF)"; font-size: 0.85em; }
a[href^="https://"]::after { content: "↗" / ""; font-size: 0.85em; }
a.pay {
display: inline-block;
padding: 0.7rem 1.2rem;
border-radius: 8px;
background: #15803d;
color: white;
font-weight: 600;
text-decoration: none;
}
a.pay:hover { background: #166534; }
</style>
</head>
<body>
<p>The <a href="/notices/exam-timetable.pdf">half-yearly exam timetable</a> is out. Uniform sizes are listed on the <a href="https://uniform-shop.example">supplier's website</a>.</p>
<p><a class="pay" href="#fees">Pay school fees</a></p>
</body>
</html>The general state rules come first in LVHA order. a.pay has the same specificity as a:link or a:visited and comes later, so it wins and keeps the button white on green in every state, while the focus ring still comes from a:focus-visible.
Quick Quiz
1. Which order of link pseudo-classes is correct?
They have equal specificity, so later rules win; LVHA lets hover and active override the resting states.
2. Why does a:visited { font-size: 2rem; } have no effect?
Limiting :visited to colours stops websites from detecting your browsing history.
3. What does content: "↗" / ""; do in an ::after rule?
The text after the slash is the alternative text for the generated content.
4. A "Delete photo" control that runs JavaScript should be built with which element?
Links navigate to URLs; actions belong in buttons, which work with the keyboard and screen readers by default.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
