Marking up a Tracklist

[I am very open to some other ideas on how best to do this!]

For my vinyl microsite, I wanted to record the tracklist of each album in my collection. Sure, it's a lot more work (though see below for some tricks), but it allows me to do some fun little things. For example, right now you may notice that the little SVG records each album generates have a different number of grooves on the front; that's not a random number or anything, it's calculated based on the tracklist for the first side of the actual vinyl. Nerdy, but neat 🤓

I've also integrated direct scrobbling to Last.FM, with the option to scrobble per side of a record, again using this data.

I figured that if I'm recording all that information on the back end, I might as well show it off, right? It provides some additional context, it makes it more accessible, and it's just interesting. So I added it to my API endpoint and pulled it onto my page template, but then I ran into a conundrum: what's the best way to mark this up?

On the one hand, a tracklist is quite obviously a list – it's literally in the name! But on the other, I'm including some additional details like track number, side placement, and track length/duration. I could throw these into the same list-item element (<li>), but it would be a single string of text. That's annoying for a couple of reasons:

  1. It's not that easy to style, not without a bunch of additional CSS and some <span> elements at least;
  2. It doesn't sound that great when you're using a screen reader.

If a list isn't ideal, does HTML have any other options? Well, yes, the very obvious and much-maligned <table> element sprung to mind.

Lists Versus Tables

Unfortunately, because of the, shall we say, chequered history of the <table> element, searching for actual advice on when to utilise it is trickier than it should be. Finding any definitive tests looking into the performance of lists versus tables was even harder. I did find some interesting research by Snook looking into definition lists versus tables but, eh, that's not quite the same thing, nor was it particularly recent[1].

Instead, I fired up NVDA and did some on-the-spot testing. This is far from exhaustive (I literally used one browser/OS/screen reader combo, it's about as weak as you can get) but it at least gave me a rough idea of how the two scenarios would play out. It also showed that some of the advice in the Snook article is definitely not relevant anymore, with some of the warnings about NVDA not matching how the program worked for me.

For the purposes of the test, I wanted to mimic as close to the visual layout I'd ultimately hoped for: a tracklist divided into sections based on the side, where each track shows its number, duration, and title.

First up is the list:

    {, index) => {
        const side = track.side === album.tracks[index - 1]?.side ? null : (
                <h3>Side {track.side}</h3>

        const duration = {
        minute: track.length.replace(/:.*$/, ''),
        second: track.length.replace(/^.*:/, '')
        return (
                {side !== null && side}
                <li key={track.number}>
                    #{track.number} {} <time dateTime={`${duration.minute}m ${duration.second}s`}>{track.length}</time>

Okay, there are some slightly odd things going on here. First off, yes, I'm writing this in React/JSX, but hopefully the templating is easy enough to follow no matter your background.

I create an unordered list[2] first, then loop through my tracks array and output a list-item for each track that contains all the necessary information. Because I want to highlight which side of the record a track is on, I had a choice to make: add that information to every list-item, or add subheadings that group tracks into sides directly. The latter felt more succinct and visually interesting, so that's what I'm using the side variable to do.

All I'm doing is comparing the current value of track.side with the value of the previous track; if they're different then we've switched sides, so I should add a headline item to the list with the new value. Otherwise, the comparison returns null and nothing is rendered.

I'm also setting a <time> element with a duration value to make the track length more machine-readable (but don't worry too much about that).

Here's what this looks like:

A bulleted list of tracks from the Death Cab for Cutie album Plans.

As for the table, it's a bit more code but the general gist is the same:

    <caption className="sr-only">Tracklist</caption>
    <thead className="sr-only">
        <th scope="col">
            Track Number
        <th scope="col">Track Title</th>
        <th scope="col">Track Length</th>
        {, index) => {
            const side = track.side === album.tracks[index - 1]?.side ? null : (
            <h3>Side {track.side}</h3>

            const duration = {
            minute: track.length.replace(/:.*$/, ''),
            second: track.length.replace(/^.*:/, '')

            return (
                    {side !== null && side}
                    <tr key={track.number}>
                            <time dateTime={`${duration.minute}m ${duration.second}s`}>{track.length}</time>

That should look pretty similar. Apart from the extra table elements like <td> and <tbody> the code structure is the same. We create our main element (<table>), map through the array of tracks, check each one to see if it belongs to a new side or not, and then either render a single table row (<tr>) or two rows, with the first being a heading.

The main difference is in the inclusion of some specific heading elements: <caption> and <thead>. The caption is effectively a title for the table, whilst the thead provides column headings. These provide some much-needed additional context for the table, but they're also not necessary for visual users, as the idea of a tracklist can be inferred based on the rest of the page. As a result, both sections have a class applied: .sr-only. This hides them visually, but keeps them accessible to screen readers and other web browsing technologies – nice 😎[3]

I've also added some scope attributes in a couple of places. Honestly, I'm not convinced they're doing anything, but MDN recommends them for help with some assistive technologies so why not, eh.

Here's what the table looks like:

A table with three columns and several headings, showing the tracks on Death Cab for Cutie's album Plans. The three columns show track number, title, and duration respectively.

How Do They Sound?

With both variations in place, I fired up NVDA and got to browsing. There are some clear benefits to each variation, but I was surprised at how similar they actually turned out. Skipping between heading sections (i.e. sides) is just as easy using either method, and both tell you how many items to expect[4] as soon as you enter them.

Lists have a tendency to announce the bullet, which is annoying. Otherwise, they're a lot quicker to navigate through, because each "row" is a single line of text that's read all at once.

On the other hand, if you're only interested in one part of that text (say the title), you have no choice but to listen to the whole thing. They also generally felt less intuitive in the way each track was described, but that's a very personal takeaway, so I wouldn't consider it too greatly.

Tables get points for ease of layout; a bunch of otherwise irritating aspects are just done for you out-of-the-box, like getting track lengths to line up neatly (though, if you don't want that, conversely it's impossible to not have it happen, so swings and roundabouts I guess). The native <caption> element is very useful too, providing greater context to non-visual users, as do the table headings (though see below for a caveat).

Unfortunately, NVDA will announce the row number for each row you enter[5], and you then have to navigate through the corresponding columns manually. This makes navigation slower, but more complete, so it's a double-edged sword. For instance, track length is not something people will care about that much, and in a table you can quickly understand the structure and skip over it with a single button press, whereas in a list you just end up having to hear it each time.

As for, table headings, they just didn't work as I expected. They only announced themselves at the start of the table, not for each row. This may be a setting for NVDA that you can tweak (there are a lot of these sorts of settings for streamlining use) and I imagine some users will appreciate the lack of repetition, but it's worth being aware of if (like me) you'd expected a bit more consistency from a table element.

Neither method picked up the track length "properly" either, despite the <time> element, which is a shame.

Here's how a list sounded in NVDA (from the top of the list to the first list-item):

> List with sixteen items, bullet
> Bullet number zero one, marching bands of Manhattan, four twelve

Compare to a table:

> Table with sixteen rows and one columns
> Caption: track list
> Track number; track title; track length
> Row three, number zero one
> Column two: marching bands of Manhattan
> Column three: four twelve

So, Table or List?

Honestly, there's no clear winner here (yay? 🎉😕)

Of course, in the case of a draw, semantics should probably win out. If your use-case makes more semantic sense as a list (or only makes semantic sense as a list) then you should use a list, and vice versa. But if, like me, either fits, then it becomes trickier to make a specific recommendation.

Personally, I found the experience with the table slightly more user-friendly. I preferred the ability to quickly skip through the content in the table I didn't care about; I found the added context provided by the table headings useful[6]; and a table will ultimately make my final layout slightly easier to achieve[7]. As a result, a table is what I've gone with, for now.

That said, if you are a screen reader user (or any other kind of user) and have some additional insight or use cases I've failed to consider, please reach out (Mastodon is probably easiest). Whilst this particular example is from a silly and fairly personal microsite that I don't expect many other people to utilise, the general pattern is one I've come across a few times, and the more information the better.

Explore Other Articles


Herding My Thoughts

After a month on the Fediverse, what parts have I grown to love, which parts would I like to see changed, and what has surprised me the most.


Want to take part?

Comments are powered by Webmentions; if you know what that means, do your thing 👍


  • <p>Is an album's tracklist better suited for a table or a list? The answer may surprise you! (But it probably won't).</p>
  • Murray Adcock.
Article permalink

Made By Me, But Made Possible By:


Build: Gatsby

Deployment: GitHub

Hosting: Netlify

Connect With Me:

Twitter Twitter

Instagram Instragram

500px 500px

GitHub GitHub

Keep Up To Date:

All Posts RSS feed.

Articles RSS feed.

Journal RSS feed.

Notes RSS feed.