Skip to content
HN On Hacker News ↗

You don’t know HTML Lists

▲ 358 points 89 comments by speckx 1w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

1 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 7 of 7
SEGMENTS · AI 0 of 7
WORD COUNT 1,601
PEAK AI % 1% · §7
Analyzed
May 16
backend: pangram/v3.3
Segments scanned
7 windows
avg 229 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,601 words · 7 segments analyzed

Human AI-generated
§1 Human · 1%

Reading Time: 13 minutesThis second installment in the “You don’t know HTML” series is going to be all about the ways that we put collections of things together. We’re skipping over the MDN and W3Schools introductory pages and instead we’re going into the kind of stuff you discover after accidentally taking your cousin’s Ritalin right before you open up the W3C specs. Let’s dive deep into lists. We’re not even talking about the ways you can style them. This isn’t an introduction!I’m assuming you’ve got real-world experience writing HTML and this isn’t your first time searching “How to make a list.” What I’m going to cover are all of the ways you can put collections of content together. So I’m talking about these kinds of lists: Ordered

Unordered

Description

Menu

Control And if you didn’t know there were five different kinds of lists in HTML, perfect. That must mean you don’t know HTML!How Do we Decide Which to Use?No need to ask AI for a summary; I’ll just give you the ending up front. Here’s how you’ll decide which kind of list to use: If the items in the list are for a single control field where you’re getting data from a user, you either want a <select> + <option> mashup or an <input> + <datalist> combo

If changing the order of the items would change the meaning of the list, then use an ordered list (<ol>)

If the items are key-value pairs, or keys-to-value pairs, use a description list (<dl>)

If the items are controls that will perform actions in the user interface, use a menu (<menu>)

Use an unordered list (<ul>) Control Lists with <select> and<option> or <input> and <datalist>When we think of lists, we don’t usually throw user control fields into the mix. And that’s weird, because we construct our navigations using lists, and those are lists of links that the user…uh…can control. So we tend to have a bias with what we think lists are. But I’m here to bring that to the forefront of your mind: when we’re building forms, sometimes we’re building lists that our users will interact with.

§2 Human · 0%

If it’s a fixed list, use <select> and <option>When I say “fixed”, I mean that the user can only choose the items from that list. If that’s the case, let’s use select and option Suppose we want a list of languages to talk in:<select name="languages"> <option value="">Select a Language</option> <option value="en">English</option> <option value="fr">French</option> <option value="es">Spanish</option> <option value="pt">Portuguese</option> </select>This gives the user exactly one choice to make. But if the user were also multilingual, maybe they’d like to choose more than one. Easy enough with the multiple attribute! The list will display differently. Now all the options will be visible so we can shift or cmd + click the ones we want:<select name="languages" multiple> <option value="">Select a Language</option> <option value="en">English</option> <option value="fr">French</option> <option value="es">Spanish</option> <option value="pt">Portuguese</option> <option value="en">Irish</option> <option value="cy">Welsh</option> </select>So long as you’re doing this with an actual select element and an option, you don’t have to use the aria-multiselectable attribute on a list element with the role="listbox" attribute. Native browser semantics bakes that in for you.Put related options together with <optgroup>What if we wanted to group languages by language-families? We can do that with optgroup which lets us group a list of options together:<select name="languages"> <optgroup label="Germanic"> <option value="en">English</option> </optgroup> <optgroup label="Romance"> <option value="fr">French</option> <option value="es">Spanish</option>

§3 Human · 0%

<option value="pt">Portuguese</option> </optgroup> <optgroup label="Celtic"> <option value="en">Irish</option> <option value="cy">Welsh</option> </optgroup> </select>What if there’s a bunch of options, but for [reasons] we don’t want a user to be able to select a subset of them? Let’s add the disabled attribute to an optgroup:<select name="languages"> <optgroup label="Germanic"> <option value="en">English</option> </optgroup> <optgroup label="Romance"> <option value="fr">French</option> <option value="es">Spanish</option> <option value="pt">Portuguese</option> </optgroup> <optgroup label="Celtic" disabled> <option value="en">Irish</option> <option value="cy">Welsh</option> </optgroup> </select>Use native HTML options first for improving the listSometimes we may want a visual break between your groups. If we don’t want to fiddle with CSS, we’re in luck! An <hr> is an approved item in a select. Not only does that make our select look a little sharper, we can also use the size attribute to control how many items will be displayed at once — making this useful for especially long lists.We just gotta watch out with size if we’re also using optgroup because those group labels will take up some of that space we were probably hoping for:<select name="languages" size="4" multiple> <optgroup label="Germanic"> <option value="en">English</option> </optgroup> <hr /> <optgroup label="Romance"> <option value="fr">French</option> <option value="es">Spanish</option> <option

§4 Human · 0%

value="pt">Portuguese</option> </optgroup> <hr /> <optgroup label="Celtic"> <option value="en">Irish</option> <option value="cy">Welsh</option> </optgroup> <hr /> <optgroup label="Afroasiatic"> <option value="he">Hebrew</option> <option value="ar">Arabic</option> </optgroup> </select>If it’s a suggested list, use <datalist>Let’s suppose we have a control where we want to suggest a list options to a user. This is where we get the datalist involved.Using a datalist is a two-step process because we have to tell the input to use a datalist. Create a datalist and give it an id.

Put the value of that id in the list attribute of a corresponding input <datalist id="languages"> <option>English</option> <option>French</option> <option>Spanish</option> <option>Portuguese</option> <option>Irish</option> <option>Welsh</option> <option>Hebrew</option> <option>Arabic</option> </datalist>

<input name="language" list="languages"> English French Spanish Portuguese Irish Welsh Hebrew Arabic

We need to watch out for using a value attribute on the <option> of a <datalist>! This isn’t a datalist problem but an option problem: The default value for an option is the text it wraps. A value attribute overrides that and then the text acts like a label. This is no big deal for a select list because the user only sees the text. But if we put a value on an <option> in a datalist the user will see the “label” in the list, but when they select it they’ll see the value in the input.

§5 Human · 1%

It’s a confusing experience. Start typing w in this input and then select “Welsh” to see what I mean:<datalist id="languages"> <option value="en">English</option> <option value="fr">French</option> <option value="es">Spanish</option> <option value="pt">Portuguese</option> <option value="en">Irish</option> <option value="cy">Welsh</option> <option value="he">Hebrew</option> <option value="ar">Arabic</option> </datalist>

<input name="language" list="languages"> English French Spanish Portuguese Irish Welsh Hebrew Arabic

So if we’re going to use a datalist, we need to work with the understanding that the value is what gets inserted — not the label. We can use a datalist for any kind of inputWe tend to think of the datalist as being useful for text options. But that ain’t how it has to work. Suppose we had a calendar widget and we wanted to gently suggest a particular range of weeks in the year. We could do that with a datalist:<label for="camp-week">Choose a week</label>

<input type="week" name="week" id="camp-week" min="2026-W2" max="2026-W51" list="preferred-weeks" />

<datalist id="preferred-weeks"> <option>2026-W22</option> <option>2026-W23</option> <option>2026-W24</option> <option>2026-W25</option> </datalist> Choose a week:

2026-W22 2026-W23 2026-W24 2026-W25 <datalist> and <input type="range"> can work together<datalist> isn’t limited to stringy values; it works with numbers. Which means we could pair it with a range input and create labeled stops along the range.

§6 Human · 1%

The only thing we have to watch out for in this approach is that not all browsers are guaranteed to work the same way. In Chrome and friends, we could display these stops with very programmatic and simple CSS. In Firefox…shenanigans are involved. But it starts with the big idea that you can display a datalist:<div class="rangeField"> <label for="tips">Tip Percentage</label>

<input type="range" name="tips" id="tips" min="0" max="50" step="1" list="recommended-tips" />

<datalist id="recommended-tips"> <option value="10" label="10%"></option> <option value="18" label="18%"></option> <option value="30" label="30%"></option> <option value="45" label="45%"></option> </datalist>

<style> .rangeField {

/* container for the two things ch is the width of the 0 in computed font. Very precise for numbers */ width: 50ch; }

/*same width for input and datalist*/ #recommended-tips, #tips { width: 100%; margin: 0; padding: 0; }

#recommended-tips { position: relative; display: block; writing-mode: vertical-lr; }

</style> </div>Tip Percentage

Our programmatic styles which will work in Chrome and friends will involve using the attr() function, casting it to a percent, and some math.@supports (x: attr(x type(percentage))) { /* For browsers that let you set a type on an attr() 1. get value from the label with attr() 2. use the type() function to declare the value as percent 3. make it absolute 4. max of input is 50, not 100.

§7 Human · 1%

Set left to be closeish to left x 2, and subtract based on the character width */ /* set datalist to display, and be a positioning root add a vertical writing mode */ #recommended-tips option{ --percent: attr(label type(<percentage>)); position: absolute; left: calc((var(--percent) * 1.9) - .1ch); } }For this to work in Firefox, we have to go in a different and more annoying direction. We will need to manually set these as separate rulesets. And we will target a pseudo-element instead. And our math gets weirder. This is not guaranteed to display well on your screen:@supports not (x: attr(x type(percentage))) { /* In firefox, the values display as a ::before Also, explicitly set the height of the option, otherwise it will be too big so set the ::before to position absolute Also, don't set length with percent as it's wildly off instead, use the same unit set on the container (ch) */

#recommended-tips option { height: 1ch; margin:0; padding:0; } #recommended-tips option::before { position: absolute; top: .5ex; } #recommended-tips option[value="10"]::before { left: calc(5ch + 2ex) }

#recommended-tips option[value="18"]::before { left: calc(9ch + 2.5ex ); }

#recommended-tips option[value="30"]::before { left: calc(15ch + 4ex); }

#recommended-tips option[value="45"]::before { left: calc(22.5ch + 6.5ex); } }Ordered Lists with <ol>Any time we have a collection of items that must be read in a particular order, we should use an ordered list. We should not let visual presentation dictate this choice. It’s not about whether the items should have numbers next to them.