The ol tag in HTML: numbered lists you can control

An ol is a list where the order is part of the meaning, and the browser numbers it for you. Four attributes change the numbers, and CSS counters let you draw them any way you like.

The <ol> tag makes an ordered list: items in a sequence the reader should follow. Each item goes in an <li>, and the browser numbers them for you. Add or delete an item and the numbers update on their own.

<ol>
  <li>Boil the water</li>
  <li>Add the tea</li>
  <li>Pour and wait</li>
</ol>

Try the attributes below. Each control changes the real list and prints the HTML it now has.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ol attribute playground</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .controls { display: flex; flex-wrap: wrap; gap: 8px 14px; font-size: 14px; }
  .controls label { display: flex; align-items: center; gap: 6px; }
  select, input[type=number] { font: inherit; padding: 3px 4px; }
  input[type=number] { width: 58px; }
  .out { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; margin-top: 12px; }
  .box { background: #fff; border-radius: 10px; padding: 10px 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, .08); }
  .box h3 { margin: 0 0 6px; font-size: 12px; color: #5b6270; text-transform: uppercase; letter-spacing: .3px; }
  #list { margin: 0; padding-left: 2.6em; line-height: 1.7; }
  #list li.picked { background: #fff3a8; }
  pre { margin: 0; font: 12.5px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap; word-break: break-word; }
</style>
</head>
<body>
<div class="controls">
  <label>type
    <select id="type">
      <option value="1">1</option><option value="a">a</option><option value="A">A</option>
      <option value="i">i</option><option value="I">I</option>
    </select>
  </label>
  <label>start <input id="start" type="number" value="1"></label>
  <label><input id="reversed" type="checkbox"> reversed</label>
  <label>3rd li value <input id="value" type="number" placeholder="none"></label>
  <label>CSS list-style-type
    <select id="css">
      <option value="">(not set)</option>
      <option>decimal-leading-zero</option><option>lower-roman</option>
      <option>upper-alpha</option><option>lower-greek</option>
    </select>
  </label>
</div>

<div class="out">
  <div class="box"><h3>Result</h3>
    <ol id="list">
      <li>Boil the water</li><li>Warm the pot</li><li class="picked">Add the tea</li>
      <li>Pour and wait</li><li>Serve</li>
    </ol>
  </div>
  <div class="box"><h3>The HTML</h3><pre id="code"></pre></div>
</div>

<script>
  const list = document.getElementById('list');
  const $ = (id) => document.getElementById(id);

  function update() {
    const type = $('type').value, start = $('start').value;
    const reversed = $('reversed').checked, value = $('value').value, css = $('css').value;

    // set or remove each attribute on the real <ol>
    if (type === '1') list.removeAttribute('type'); else list.setAttribute('type', type);
    if (start === '' || (start === '1' && !reversed)) list.removeAttribute('start');
    else list.setAttribute('start', start);
    list.reversed = reversed;
    const third = list.children[2];
    if (value === '') third.removeAttribute('value'); else third.setAttribute('value', value);
    list.style.listStyleType = css;   // CSS wins over the type attribute

    // print the markup the list now has
    const attrs = [...list.attributes]
      .filter((a) => a.name !== 'id' && a.name !== 'style')
      .map((a) => a.value === '' ? ' ' + a.name : ` ${a.name}="${a.value}"`).join('');
    const items = [...list.children].map((li) => {
      const v = li.getAttribute('value');
      return `  <li${v !== null ? ` value="${v}"` : ''}>${li.textContent}</li>`;
    }).join('\n');
    const style = css ? `<style>ol { list-style-type: ${css}; }</style>\n` : '';
    $('code').textContent = `${style}<ol${attrs}>\n${items}\n</ol>`;
  }

  document.querySelectorAll('select, input').forEach((el) => el.addEventListener('input', update));
  update();
</script>
</body>
</html>
Change type, start, reversed and the third item's value. The CSS list-style-type box overrides type.

If the order of your items does not matter, you want a <ul> instead. The ul tag guide covers bullets, indents and ::marker, which work the same way on both lists.

The four attributes: type, start, reversed, value

Three attributes go on the <ol> and one on an <li>. None needs CSS.

What each attribute does to the same three items.
What each attribute does to the same three items.
Attribute Goes on What it does Example
type ol Numbers, letters or Roman numerals type="A" gives A, B, C
start ol The first number start="5" gives 5, 6, 7
reversed ol Counts down 3 items give 3, 2, 1
value li Sets this item's number Later items continue from it

start always takes a whole number, even on a lettered list. <ol type="a" start="3"> begins at c. reversed is a boolean attribute: writing it is enough, and reversed="false" still turns it on.

A common use of start is a list split by a paragraph or an image. End the first <ol>, add the paragraph, then open the second one with start set to the next number.

Number styles with list-style-type

The type attribute offers five styles. CSS offers many more through list-style-type, and when both are set, the CSS wins.

ol.chapters { list-style-type: decimal-leading-zero; } /* 01, 02, 03 */
ol.appendix { list-style-type: upper-alpha; }          /* A, B, C */
ol.clauses  { list-style-type: lower-roman; }          /* i, ii, iii */

Other values include lower-greek, upper-roman and lower-alpha. Changing only the colour or size of the numbers does not need a new style. Use li::marker { color: ...; font-weight: 700; } and the numbers stay the browser's own.

Styled number badges with CSS counters

::marker accepts only a few properties, so it cannot draw a coloured circle. For that, hide the built-in numbers and print your own with a CSS counter in ::before. Three properties do it:

.steps { list-style: none; padding: 0; counter-reset: step; }
.steps li { counter-increment: step; position: relative; padding-left: 38px; }
.steps li::before {
  content: counter(step);
  position: absolute; left: 0; top: 0;
  width: 28px; height: 28px; border-radius: 50%;
  background: #2563eb; color: #fff;
  display: grid; place-items: center;
}

counter-reset creates the counter at 0 on the list. counter-increment adds 1 on each item. counter(step) prints the current value. The ::before and ::after guide covers the pseudo-element itself.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CSS counters for ol</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .box { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, .08); font-size: 14px; }
  .box h3 { margin: 0 0 10px; font-size: 12px; color: #5b6270; text-transform: uppercase; letter-spacing: .3px; }

  /* 1. Circle badges: one counter, drawn by ::before */
  .badges { list-style: none; margin: 0; padding: 0; counter-reset: step; }
  .badges li { counter-increment: step; position: relative; padding: 4px 0 4px 38px; min-height: 28px; margin-bottom: 8px; }
  .badges li::before {
    content: counter(step);
    position: absolute; left: 0; top: 0;
    width: 28px; height: 28px; border-radius: 50%;
    background: #2563eb; color: #fff; font-weight: 700;
    display: grid; place-items: center;
  }

  /* 2. Nested 1.1 / 1.2: every ol resets its own counter, counters() joins them */
  .outline, .outline ol { list-style: none; counter-reset: item; margin: 0; padding-left: 0; }
  .outline ol { padding-left: 20px; }
  .outline li { counter-increment: item; margin: 3px 0; }
  .outline li::before { content: counters(item, ".") " "; font-weight: 700; color: #0f5132; margin-right: 4px; }
</style>
</head>
<body>
<div class="grid">
  <div class="box"><h3>Badges</h3>
    <ol class="badges">
      <li>Download the file</li>
      <li>Open it in a browser</li>
      <li>Check every page</li>
      <li>Send the link</li>
    </ol>
  </div>
  <div class="box"><h3>1.1 / 1.2</h3>
    <ol class="outline">
      <li>Setup
        <ol><li>Install</li><li>Sign in</li></ol>
      </li>
      <li>Daily use
        <ol><li>Open a file</li><li>Share it
          <ol><li>Copy link</li></ol></li></ol>
      </li>
      <li>Help</li>
    </ol>
  </div>
</div>
</body>
</html>
Left: one counter drawn as circles. Right: counters() builds 1.1 and 1.2 for nested lists.

Nested numbering: 1.1, 1.2 with counters()

A nested <ol> normally restarts at 1 with no link to its parent. For outline numbers such as 2.1, use counters(), with an s. It prints every counter of that name from the outermost list inwards, joined by the string you pass.

.outline, .outline ol { list-style: none; counter-reset: item; }
.outline li { counter-increment: item; }
.outline li::before { content: counters(item, ".") " "; }

The key is the selector on the first line. Every <ol>, nested ones included, needs its own counter-reset. Reset only the outer list and all levels share one counter.

Reset the counter on every ol, or the numbers run on through all levels.
Reset the counter on every ol, or the numbers run on through all levels.

start, reversed and value with custom counters

Once you print your own counter, the list attributes no longer reach it. start, reversed and value feed the list's built-in numbering, and counter-reset: step created a new counter.

A custom counter ignores start. Set its first value in counter-reset instead.
A custom counter ignores start. Set its first value in counter-reset instead.

Rebuild each one in CSS:

/* start="5": reset to one less than the first number */
.steps { counter-reset: step 4; }

/* reversed on 3 items: start at 4, count down by 1 */
.steps.down { counter-reset: step 4; }
.steps.down li { counter-increment: step -1; }

/* value="10" on one item */
.steps li.jump { counter-set: step 10; }

A countdown this way needs the item count, so it has to change when items do. If the list changes often, keep the built-in numbers and style them with ::marker.

A finished example: recipe steps

Steps are the classic ordered list. This recipe draws its numbers with a counter, so JavaScript never writes a number. Remove a step or add one, and every badge after it renumbers.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recipe steps</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #fbf7f2; color: #2b2118; }
  .card { max-width: 520px; margin: 0 auto; background: #fff; border-radius: 14px; padding: 16px 18px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); }
  h2 { margin: 0; font-size: 20px; }
  .meta { margin: 2px 0 12px; font-size: 13px; color: #7a6a5a; }

  .steps { list-style: none; margin: 0; padding: 0; counter-reset: step; }
  .steps li {
    counter-increment: step;
    display: flex; align-items: flex-start; gap: 12px;
    padding: 8px 0; border-top: 1px solid #f0e8de; line-height: 1.45;
  }
  .steps li::before {
    content: counter(step);
    flex: none; width: 30px; height: 30px; border-radius: 50%;
    background: #e8590c; color: #fff; font-weight: 700;
    display: grid; place-items: center;
  }
  .steps li span { flex: 1; padding-top: 4px; }
  .steps li button { flex: none; border: 0; background: none; color: #a8998a; font-size: 20px; line-height: 1; cursor: pointer; padding: 4px 6px; }

  form { display: flex; gap: 8px; margin-top: 12px; }
  form input { flex: 1; min-width: 0; font: inherit; padding: 8px 10px; border: 1px solid #ddd0c2; border-radius: 8px; }
  form button { font: inherit; font-weight: 600; padding: 8px 14px; border: 0; border-radius: 8px; background: #e8590c; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<div class="card">
  <h2>Pancakes</h2>
  <p class="meta" id="count"></p>
  <ol class="steps" id="steps" role="list">
    <li><span>Whisk flour, sugar and a pinch of salt.</span><button aria-label="Remove step">&times;</button></li>
    <li><span>Beat in the milk and eggs until smooth.</span><button aria-label="Remove step">&times;</button></li>
    <li><span>Rest the batter for ten minutes.</span><button aria-label="Remove step">&times;</button></li>
    <li><span>Cook each pancake until bubbles appear, then flip.</span><button aria-label="Remove step">&times;</button></li>
  </ol>
  <form id="add">
    <input id="text" placeholder="New step, e.g. Serve with syrup" aria-label="New step">
    <button>Add step</button>
  </form>
</div>

<script>
  const steps = document.getElementById('steps');
  const count = document.getElementById('count');

  // the numbers come from the CSS counter; JS only updates the total
  const showCount = () => { count.textContent = steps.children.length + ' steps'; };

  // one listener handles every remove button, including ones added later
  steps.addEventListener('click', (e) => {
    const btn = e.target.closest('button');
    if (btn) { btn.closest('li').remove(); showCount(); }
  });

  document.getElementById('add').addEventListener('submit', (e) => {
    e.preventDefault();  // stay on the page
    const input = document.getElementById('text');
    const text = input.value.trim();
    if (!text) return;
    const li = document.createElement('li');
    li.innerHTML = '<span></span><button aria-label="Remove step">&times;</button>';
    li.querySelector('span').textContent = text;
    steps.append(li);
    input.value = '';
    showCount();
  });

  showCount();
</script>
</body>
</html>
Remove or add steps. The numbers come from CSS, so they are always in order.
  • Numbers from CSS: the counter counts whatever <li> elements exist, so there is nothing to recalculate.
  • One click listener: it sits on the <ol> and finds the clicked button, so new steps work.
  • Still a list: the <ol> has role="list", covered below.

When it does not work

What you see Cause Fix
No numbers at all A CSS reset set list-style: none on ol Set list-style: decimal on the lists that need numbers
Numbers cut off at the left edge padding: 0 on the ol, markers sit outside the item Restore padding-left, or use list-style-position: inside
start or value does nothing Numbers come from your own counter Set the value in counter-reset or counter-set
reversed still counts up Same cause counter-increment: step -1 with a matching reset
Nested items show 3, 4, 5 instead of 1.1 counter-reset only on the outer ol Reset on every ol, and print with counters()
type="i" shows 1, 2, 3 A list-style-type rule in CSS overrides it Remove the rule or set the style in CSS

The last issue is quieter. Safari with VoiceOver may not announce a list whose markers were removed with list-style: none, so a listener loses "list, 4 items". Adding role="list" to the <ol> restores it. Lists without bullets explains the details.

A numbered guide is easier to follow when the reader can try it. A screenshot of the recipe cannot add a step, and an .html attachment may open as plain code on a phone.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can add and remove steps themselves. If you change the code later, the same link shows the new version.

Questions people ask

What is the ol tag in HTML?

ol stands for ordered list. It wraps li elements, one per item, and the browser numbers them 1, 2, 3 in the order they appear. Use it when moving an item would change the meaning, such as steps, rankings or a table of contents.

What are the type values for an ol?

type="1" gives numbers (the default), type="a" and type="A" give lowercase and uppercase letters, and type="i" and type="I" give lowercase and uppercase Roman numerals. A list-style-type set in CSS takes priority over the type attribute.

How do I start an ordered list at a different number?

Add start with a whole number, for example <ol start="5">. Write the number even when the list uses letters: start="3" with type="a" begins at c. To change the number of a single item and everything after it, use value on that li.

Why does start stop working after I style the numbers with CSS counters?

start, reversed and value set the list's own numbering. A counter you create with counter-reset is a separate counter that those attributes do not touch. Set its first value in CSS instead, for example counter-reset: step 4 to begin at 5.

Should I use ol or ul?

Ask whether reordering the items would change the meaning. If it would, as with steps, use ol. If the items are a set, such as features or ingredients, use ul. Both hold the same li element.

Keep reading