Styling Links

CSS · Lesson 17 of 55 8 min read Updated

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.

A link can be in several states at once, and each has a pseudo-class (you met these in Combinators & Pseudo-classes):

Pseudo-classMatches
:linkA link the user has not visited yet
:visitedA link to a page already in the browser history
:any-linkBoth of the above: any <a> that has an href
:focus-visibleThe focused link, when a visible ring is helpful (mainly keyboard use)
:hoverThe mouse pointer is over the link
:activeThe 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:

  1. :link and :visited (the resting states)
  2. :focus-visible and :hover (the user is pointing at it)
  3. :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.

HTMLTry it yourself
<!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 (not border) for the ring: it does not change the link's size, so nothing on the page moves.
  • outline-offset adds 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:

CSS
.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-thickness sets the line's thickness. em values grow with the text; from-font uses the thickness suggested by the font.
  • text-underline-offset moves the line down, away from the letters.
  • text-decoration-color can differ from the text colour. Here it is a softer version of the link colour, made with color-mix().
  • text-decoration-skip-ink: auto is 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.

Attribute selectors can find special links for you, so authors never have to add classes by hand:

SelectorMatches 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.

HTMLTry it yourself
<!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.

"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 (or inline-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, :active and :focus-visible styles;
  • 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. :hover written before :visited never shows on visited links. Follow LVHA.
  • Removing the focus outline. Always give keyboard users a visible :focus-visible style.
  • Removing underlines from links in paragraphs. Colour alone is not enough; refine the underline instead.
  • Styling :visited with 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-link matches every <a> with an href; :visited can only change colours.
  • Use :focus-visible with outline and outline-offset for keyboard focus rings.
  • Keep underlines on links in text and refine them with text-decoration-thickness, text-underline-offset and text-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
HTML
<!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?

2. Why does a:visited { font-size: 2rem; } have no effect?

3. What does content: "↗" / ""; do in an ::after rule?

4. A "Delete photo" control that runs JavaScript should be built with which element?

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