Skip to content

a11y: default and grid listings emit empty <a> when an item description contains a link (link-name) #15211

Closed

Description

@cderv

When a listing item description contains a link, default and grid listings render empty, focusable <a> elements. axe-core reports them as link-name, keyboard users tab onto invisible links, and part of the item is no longer a link to the item.

This is a regression from 1.7. With this website (_quarto.yml with project: type: website):

---
title: "Listing"
listing:
  - id: default-listing
    type: default
    contents:
      - path: https://example.com/article
        title: "Article"
        description: "Read the article, see the [docs](https://example.org/docs)."
  - id: grid-listing
    type: grid
    contents:
      - path: https://example.com/article
        title: "Article"
        description: "Read the article, see the [docs](https://example.org/docs)."
---

::: {#default-listing}
:::

::: {#grid-listing}
:::

Quarto 1.6.43 renders the description link as text, inside a single link to the item:

<div class="listing-description">
<a href="https://example.com/article" class="no-external">Read the article, see the <span>docs</span>.</a>
</div>

Quarto 1.10.19 keeps the description link, nested in the item link. The browser splits the nested anchors, so the item link is left empty, and in the grid listing the card link is closed before the description:

<div class="delink listing-description"><a href="https://example.com/article" class="no-external">
</a><p><a href="https://example.com/article" class="no-external">Read the article, see the </a><a href="https://example.org/docs">docs</a>.</p>
</a><div class="card-text listing-description delink"><a href="https://example.com/article" class="quarto-grid-link">
</a><p><a href="https://example.com/article" class="quarto-grid-link">Read the article, see the </a><a href="https://example.org/docs">docs</a>.</p>

The delink filter is there for this. It turns links inside a Div with the delink class into spans, so the whole description stays one link to the item:

function delink()
return {
-- Removes links from any divs marked with 'delink' class
Div = function(div)
if _quarto.format.isHtmlOutput() and div.attr.classes:includes(kDelinkClass) then
-- remove the delink class
for i, clz in ipairs(div.attr.classes) do
if clz == kDelinkClass then
div.attr.classes:remove(i)
end
end
-- find links and transform them to spans
-- this is in post, so it's after render, so we don't need to worry about
-- custom ast nodes
return _quarto.traverser(div, {
Link = function(link)
return pandoc.Span(link.content)
end
})
end
end
}
end

It only matches Pandoc Divs. Up to 1.6 the templates wrote <div class="delink listing-description"> as HTML in the Markdown, which Pandoc parses into a Div. f9a5250 and 43ff451 (for #11758) moved it into a raw HTML block, so there is no Div anymore and the filter does nothing. The delink class is even left in the output, as the filter removes it when it runs.

<% if (fields.includes('description')) { %>
```{=html}
<div class="delink listing-description"><a href="<%- item.path %>" class="no-external-icon">
```
<%= item.description %>
```{=html}
</a></div>
```
<% } %>

<% if (showField('description')) { %>
```{=html}
<div class="card-text listing-description delink">
```
<%= item.description %>
```{=html}
</div>
```
<% } %>

We could write this container as a fenced div again (::: {.delink .listing-description}), keeping only the item link as raw HTML, like the ::: {.body} div the template already uses. This gives the 1.6 output back.

Description links not being clickable is intended, see #10814 where making them clickable is discussed as an option. The table listing is not affected, as its description is not inside a link.

Related: #14380, #15129, #15066 (other empty links in default listings), #9469 (its listing axe test excludes these empty links for now).

Activity

  1. added this to the v1.11 milestone on Oct 27, 2026
  2. added a commit that references this issue on Oct 27, 2026
    aad85b2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilitybugSomething isn't workinglistingsregressionFunctionality that used to work but now is broken.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions