Mert Özen Further With Every Line
Frontend HTML5 and Semantic HTML — #03

Text Semantics: Headings, Paragraphs, Lists, Links, Buttons and Inline Elements

Mert Özen Nov 14, 2026 12 min 4 views
Text Semantics: Headings, Paragraphs, Lists, Links, Buttons and Inline Elements

Something that takes the user to another address is an a, something that performs an action on the page is a button; href="#" and a clickable div work only partly or not at all with a keyboard. In this post we look at strong/em versus b/i, book details with dl, time, abbr, code/kbd/samp, blockquote/q/cite and figure in Chromium's accessibility tree, measure link versus button with a real keyboard test, and check the implicit noopener behaviour of target="_blank" in two engines.

Version note: This post follows the HTML (HyperText Markup Language) Living Standard and Baseline as of November 2026; the examples were run with Chrome 158, WebKit 27.2 (Playwright 1.65) and Nu Html Checker 26.11.9.

Guess First

On Bookstore's book detail page, three different teams wrote three different "Add to cart" controls:

<a href="#" onclick="addToCart('a')">Add to cart</a>
<div class="btn" onclick="addToCart('div')">Add to cart</div>
<button type="button" onclick="addToCart('button')">Add to cart</button>

With CSS (Cascading Style Sheets) all three look like the same blue button, and all three work with a mouse. When someone who does not use a mouse moves through the page with Tab and presses Enter or Space, which ones add the book to the cart? And what does the href="#" in the first line do to the page address?

In the previous post we named the big blocks of the page with landmarks. In this post we go inside those blocks, into the text: emphasis, a book's bibliographic details, quotations, code, and the most frequently wrong decision, link versus button. We will read every claim from Chromium's accessibility tree or from a real keyboard test.

The Emphasis Family: strong, em, b, i, mark and small

All of them show text as bold, italic or highlighted; the difference is not in the look but in what they say. Here is how I put the spec's definitions into my own words:

strong signals importance, seriousness or urgency: like "non-refundable" in "This book is non-refundable". em is stress emphasis, where moving the stress changes the meaning of the sentence: in "This book is not a framework tutorial", stressing "framework" and stressing "not" say different things; the first implies it is a tutorial about something else, the second pushes back on someone who thought it was a framework tutorial. b draws attention without adding importance: keywords in a summary, a product name in a review. i marks a voice that differs from the surrounding text: a foreign term, a thought, the name of a ship. mark highlights relevance in the current context; its most typical use is marking the searched word in search results. small is for side comments and small print: "Prices include VAT (Value Added Tax)."

In practice two rules are enough. First, if all you want is a look, use CSS rather than an element: a bold price on a book card is a design decision written with font-weight. Second, do not sprinkle strong everywhere as a "bold shortcut"; if everything is important, nothing is. Do not rely on screen readers announcing these emphases either; most do not do it with their default settings. The sentence itself should carry the meaning.

The Book's Details: dl, dt, dd and time

The most structured part of a book detail page is the bibliographic block: author, publication date, ISBN (International Standard Book Number), formats. These are name–value pairs, and HTML has an element for them: the description list.

<dl>
    <dt>Author</dt>
    <dd>Robert C. Martin</dd>
    <dt>Published</dt>
    <dd><time datetime="2017-09-10">10 September 2017</time></dd>
    <dt>ISBN</dt>
    <dd>978-0134494166</dd>
    <dt>Formats</dt>
    <dd>Paperback</dd>
    <dd>E-book</dd>
</dl>

dt holds the name, dd the value, and one dt can have several dd elements, as in the "Formats" row. The spec also lets you wrap each pair in a div for styling, which makes a two-column grid layout easy. You could have built this with a table or with <p><b>Author:</b> …</p> lines; the difference is that a screen reader can present it as a list of term–definition pairs and machines know which value belongs to which name.

I used time for the date. The format for humans is on the screen, the format for machines is in the datetime attribute. On a Turkish page you write "10 Eylül 2017" on screen and datetime stays 2017-09-10. The value must be in a format the spec recognizes; when I wrote datetime="10/09/2017", the checker rejected it:

:5.14-5.41: error: Bad value “10/09/2017” for attribute “datetime” on element “time”: The literal did not satisfy the time-datetime format.

This error catches a real ambiguity: is 10/09 the 9th of October or the 10th of September? The ISO (International Organization for Standardization) 8601 format removes the question. When you produce this value from ASP.NET Core on the server, use a culture-independent format such as ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); a date formatted with the Turkish culture must not end up here.

Quotations, Abbreviations, Code and Figure

The description and review part below the details shows the rest of text semantics:

<p>Run <code>dotnet new sln</code> and press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd> to build.
    The terminal prints <samp>Build succeeded.</samp></p>
<blockquote cite="https://example.com/reviews/42">
    <p>The chapters on boundaries changed how I split modules.</p>
</blockquote>
<p>The review title says <q>Worth reading twice</q> and compares the book with
    <cite>Refactoring</cite>.</p>
<p><mark>In stock</mark> <small>Prices include VAT.</small></p>
<p>The <abbr title="Single Responsibility Principle">SRP</abbr> chapter is short.</p>
<figure>
    <img src="cover.jpg" alt="Cover of Clean Architecture" width="200" height="300">
    <figcaption>First edition cover</figcaption>
</figure>

code marks a piece of code, kbd a key the user presses or input they type, samp output produced by a program. For multi-line code you put code inside pre; pre preserves whitespace and line breaks, code carries the meaning. For a team writing technical documentation, this trio builds the "what I type versus what I see" distinction into the text.

blockquote is a block quotation from another source; its cite attribute carries the source URL (Uniform Resource Locator) but is not shown to the user, so if you want it visible, write the source into the text as well. For a short inline quotation there is q; the browser adds the quotation marks itself through CSS, you do not type them. The cite element, on the other hand, is for the title of a work: a book, a film, an article. The spec explicitly says that a person's name is not the title of a work, so it must not be marked up with cite; the author's name is plain text.

abbr marks an abbreviation and can give its expansion in title. But do not rely on title: it is invisible until you hover with a mouse, never visible on a touch screen, and screen readers do not read it consistently. The right approach, as in the posts on this site, is to expand the abbreviation in the text where it first appears; abbr is a marker added on top of that.

figure is self-contained content referenced from the main text that keeps its meaning even if moved: a book cover, a diagram, a code sample. figcaption is its caption and must be the first or last child of the figure. The image's alt and the figcaption should not repeat each other: alt says what is in the image, figcaption says why it is there. Images are covered in detail in the series' post on images and media.

How Does the Browser See Them?

I gave the book page containing all these elements to Chromium, using a version of the previous post's script with a looser role filter. With generic, paragraph and plain text nodes hidden, this tree remains:

main
  article
    heading "Clean Architecture"
    DescriptionList
      term "Author"
      definition
      term "Published"
      definition
        time
      term "ISBN"
      definition
      term "Formats"
      definition
      definition
    strong
    emphasis
    code
    blockquote
    mark
    Abbr "Single Responsibility Principle"
    figure
      image "Cover of Clean Architecture"
      Figcaption

Two things stand out. First, the details block is now a real structure: term and definition pairs, with two definition entries under "Formats". Second, what is missing from the list says something too: b, i, small, kbd, samp, q and cite get no separate role in Chromium's tree. That does not make them meaningless; their meaning is in the spec and serves software that processes text, such as search engines, reading views and translation tools. But they carry nothing special for assistive technology. Choosing the right element never hurts; choosing the wrong one always breaks something somewhere. The clearest example is in the next section.

The rule fits in one sentence: if it takes the user somewhere else, it is an a; if it does something on the page, it is a button. "View cart" goes to an address, a link; "Add to cart" adds a book to the cart, a button. I put the three variants from the guessing question on one page together with a "View cart" link:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Book actions | Bookstore</title>
</head>
<body>
    <a href="/cart">View cart</a>
    <a href="#" onclick="addToCart('a')">Add to cart</a>
    <div class="btn" onclick="addToCart('div')">Add to cart</div>
    <button type="button" onclick="addToCart('button')">Add to cart</button>
    <script>
        const clicks = [];
        function addToCart(source) {
            clicks.push(source);
        }
    </script>
</body>
</html>

To try this with a keyboard in a real browser, I wrote a Playwright script. It serves the page from a small HTTP (Hypertext Transfer Protocol) server, moves through it with Tab printing the focused element, then presses Enter and Space and counts which clicks happened:

const { chromium } = require('playwright');
const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  res.end(fs.readFileSync('actions.html'));
}).listen(18878);

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('http://localhost:18878/book/42');

  // Which elements can Tab reach?
  for (let i = 1; i <= 4; i++) {
    await page.keyboard.press('Tab');
    const focused = await page.evaluate(() => {
      const el = document.activeElement;
      return el === document.body ? 'body' : `${el.tagName.toLowerCase()} "${el.textContent}"`;
    });
    console.log(`Tab ${i}: ${focused}`);
  }

  // Do Enter and Space trigger a click?
  for (const selector of ['a[href="#"]', 'button']) {
    for (const key of ['Enter', ' ']) {
      await page.focus(selector);
      await page.keyboard.press(key === ' ' ? 'Space' : key);
    }
  }
  console.log('clicks:', await page.evaluate(() => clicks));
  console.log('url:', page.url());
  console.log('roles:', await page.getByRole('button', { name: 'Add to cart' }).count(), 'button,',
    await page.getByRole('link', { name: 'Add to cart' }).count(), 'link');

  await browser.close();
  server.close();
})();
Tab 1: a "View cart"
Tab 2: a "Add to cart"
Tab 3: button "Add to cart"
Tab 4: body
clicks: [ 'a', 'button', 'button' ]
url: http://localhost:18878/book/42#
roles: 1 button, 1 link

Let us read the output line by line. The div never entered the Tab order: at Tab 3 focus went straight to the button, at Tab 4 it left the page. For a keyboard user, the div "Add to cart" does not exist. The link with href="#" is reachable with Tab and works with Enter, but not with Space: the clicks list has only one 'a'. On a link, Space scrolls the page. The button fired twice, once for Enter and once for Space. On top of that, the link polluted the address: a # was appended to the URL, a new entry went into the browser history, and the page may have scrolled to the top. The last line tells you what a screen reader hears: of the same text, one is announced as a button, one as a link, and the div as nothing at all.

Can the div be saved? Yes, if you add role="button", tabindex="0" and a keydown handler for Enter and Space. But that means rewriting in three pieces of code what the browser gives you for free with button; and you will forget the disabled state, the behaviour inside forms and high contrast mode. How to do it with ARIA (Accessible Rich Internet Applications) is a topic of the Accessibility (a11y) series; the answer here is short: write a button.

The rule is the same in Angular. Everything that changes the page should be an a with routerLink; Angular generates the href itself, so opening in a new tab and copying the link address work. When there is a div or span with a (click) handler, angular-eslint's click-events-have-key-events and interactive-supports-focus template rules catch it; putting those rules into CI (Continuous Integration) is a topic of the upcoming Static Analysis series. The Nu Html Checker catches neither of these two examples: div onclick and href="#" are valid according to the spec, just wrong. The only variant the checker catches is nesting a button inside a link:

:4.17-4.38: error: The element “button” must not appear as a descendant of the “a” element.

target="_blank" and Implicit noopener

On Bookstore, the "Preview" link opens the PDF (Portable Document Format) preview in a new tab: <a href="/preview" target="_blank">. An old warning says to always add rel="noopener" to such links, because the page in the new tab could reach the opening page through window.opener and redirect it to another address. Today browsers apply noopener behaviour to target="_blank" by default. I measured it in two engines; the second link deliberately turns the behaviour off with rel="opener":

chromium plain: window.opener is null
chromium opener: window.opener is set
webkit plain: window.opener is null
webkit opener: window.opener is set

So rel="noopener" is no longer required; writing it is harmless. rel="noreferrer" does something else: it does not send the Referer header to the target page. Which one you want when, and the security model of third-party links, is in the Frontend Security series post on third-party scripts, iframes and postMessage. A UX (User Experience) note as well: a link that opens in a new tab breaks the user's back button; use it only when really needed, for example for a help page opened while filling in a form.

Lists, Headings and br

A navigation menu is a list of links and is written as a ul inside nav; the screen reader tells the user how long the menu is by announcing it as a list with a number of items. Use ol wherever order carries meaning: checkout steps, installation instructions, "this author's 5 bestselling books". The start and reversed attributes of ol change the numbering without touching CSS.

Choosing a heading for its visual size is the oldest mistake in this family. Turning the "Bestseller" label on a book card into an h4 so it looks big adds a meaningless line to the screen reader's heading list and breaks the level structure we built in the previous post. The heading level defines the structure; CSS defines the look.

br is only for line breaks that are part of the content: the lines of an address, the lines of a poem. Writing <br><br> to leave space between two paragraphs gives a screen reader one long paragraph; spacing is the job of CSS, a paragraph is the job of p. There is also hr to show a change of topic; its name says horizontal rule, but its meaning is "thematic break".

In Production

The biggest enemy of text semantics is rich text editors and CMSs (Content Management System). If Bookstore's book descriptions come from publishers as HTML, you will find paragraphs full of <b>, <br><br>, <font> and inline styles. Cleaning that content with an allow-list of elements before putting it on the page is a must for both security and semantics; run the Nu Html Checker on samples of that content too.

Make the link versus button decision at the level of the design system. In Bookstore's UI (User Interface) library, keep "a link that looks like a button" and "a button that looks like a link" as separate components, and let the component name describe the behaviour, not the element. When a designer says "this is a button", the question to ask is "where does it go when clicked?"; if the answer is an address, the element is a, regardless of how it looks.

To measure, take a tour with the keyboard: let go of the mouse and try to use the page with only Tab, Shift+Tab, Enter and Space. Doing by hand what the script above does takes five minutes and immediately exposes the div buttons that slip through design reviews.

Common Pitfalls

First, writing actions with <a href="#"> or <a href="javascript:void(0)">. It does not work with Space, pollutes the address and history, and gives the screen reader the wrong role. If it is an action, it is a button.

Second, clickable div and span elements. They cannot be reached with the keyboard and the Nu Html Checker does not catch them; lint rules and a keyboard test will.

Third, using strong and headings for their looks. Weight and size are CSS; the element carries meaning.

Fourth, writing a culture-dependent date into the datetime of time. The value machines read is always ISO 8601.

Fifth, making abbr title the only place for the expansion. It is invisible on touch screens and in most screen readers; expand the abbreviation in the text where it first appears.

A Small Experiment

★☆☆ Save the book page from this post, open it in Chrome and look at the roles of the dl, time and abbr elements one by one in the Accessibility tab of the Elements panel in DevTools. Then rewrite the details block with p and b and compare the difference.

★★☆ Make the div in actions.html keyboard-operable by adding role="button", tabindex="0" and a keydown handler. Run keyboard.js again and confirm that its clicks entries match the button's. Count how many lines you wrote, then how many it takes to delete them and put a single button in their place.

★★★ Find every div, span and a with href="#" carrying (click) or onclick in your project (a grep is enough). Classify each as a or button by asking "where does it go / what does it do", and fix the three most used ones.

In the next post we move from text to forms: Bookstore's checkout form. The right input type, label, fieldset, autocomplete and inputmode so the mobile keyboard opens correctly, and why the default type of button is submit.