All posts

Markdown Bullet Points and Nested Lists

Markdown bullets can be a hyphen, asterisk, or plus sign. Here's how nesting, indentation, loose versus tight lists, and multi-paragraph list items actually work.

Short answer: Markdown bullets are a hyphen, asterisk, or plus sign followed by a space — - item, * item, or + item. All three render identical bullet points. To nest a list, indent the child items by two spaces (or align them under the parent's text). Switching markers mid-list starts a brand new list rather than continuing the old one.

Lists are the most-used markdown feature after headings, and the one with the most surprising edge cases. Indentation that works on GitHub breaks in your static site generator. A stray blank line adds spacing you didn't ask for. A code block inside a list item silently escapes the list. Here's what's actually going on.


The Three Markers

- Hyphen
* Asterisk
+ Plus sign

All three produce the same <ul> with <li> children. There's no hierarchy implied — a * bullet is not a sub-bullet of a - bullet.

Which should you use? - is the modern convention — Prettier, most linters, and most style guides normalize to it. * collides visually with emphasis markers, making dense source harder to scan. + is rare enough that it mostly signals an unusual toolchain. Pick one and stay with it across a document.


Consistency Matters More Than You Expect

Changing the marker doesn't just look inconsistent — it changes the output. In CommonMark and GFM, a different bullet character starts a new list:

- First
- Second
* Third
* Fourth

That's two separate <ul> elements, not one four-item list. Visually you may not notice, but the spacing between "Second" and "Third" changes, and anything that walks the document structure — a table of contents generator, a screen reader, a CSS rule targeting ul + ul — sees two lists.

The same is true for ordered lists: switching between 1. and 1) starts a new list.


Nesting and the Indentation Question

To nest, indent the child items:

- Parent item
  - Child item
  - Another child
    - Grandchild
- Second parent

How much indentation? This is where markdown's history shows. The original 2004 implementation wanted four spaces. CommonMark and GFM use a different rule: child content must be indented to at least the column where the parent item's content starts.

For a - bullet, the content starts at column 2, so two spaces is enough:

- Parent
  - Child

For an ordered item like 1. , the content starts at column 3, so you need three:

1. Parent
   - Child

For 10. , it's four.

Four spaces works for hyphen bullets too, because "at least" means anything deeper also qualifies — up to a limit. Indent a child more than three spaces past the parent's content column and it becomes an indented code block inside the list item instead of a nested list. That's the single most common cause of "my nested list turned into a grey box."

Practical rule: indent each level so the child's marker lines up under the first character of the parent's text. That satisfies CommonMark, satisfies older four-space parsers for hyphen lists, and reads correctly in the raw source.

- Install dependencies
  - macOS: `brew install node`
  - Linux: use your package manager

Mixing Ordered and Unordered

Nesting one type inside the other works in both directions:

1. Clone the repository
2. Configure the environment
   - Copy `.env.example` to `.env`
   - Add your API key
3. Run the test suite

Numbered steps with unordered detail under each is the most common documentation pattern, and it renders correctly everywhere. The reverse — a numbered list nested under a bullet — works the same way.


Loose Versus Tight Lists

This one confuses people because the difference is invisible in the source but obvious in the output.

A tight list has no blank lines between items. Each item's content goes directly inside the <li>, and the output is compact.

A loose list has a blank line somewhere between items:

- Apples

- Oranges

- Pears

Every item's content gets wrapped in a <p> tag — which means paragraph margins, which means visibly more vertical space between items.

The rule catches people out because one blank line anywhere makes the entire list loose. You can't have some items tight and others loose. If you add a second paragraph to one item, that item needs a blank line, and now every item in the list is <p>-wrapped.

- Apples
- Oranges

  Also tangerines and clementines.

- Pears

All three items are now paragraph-wrapped, even the two that only have one line. That's correct behavior, not a bug, but it explains why a list suddenly "got taller" when you added detail to one item.

If you want compact output, keep all blank lines out. If you need multi-paragraph items, accept the loose rendering.


Paragraphs and Code Blocks Inside List Items

To attach extra content to a list item, indent it to the item's content column and separate it with a blank line:

1. Install the package

   This step downloads about 40MB of dependencies. It may take a
   minute on a slow connection.

   ```bash
   npm install
   ```

2. Run the migration

Three spaces of indentation, matching 1. . The paragraph, the fenced code block, and anything else you indent to that column stay inside the list item.

Under-indent and the content breaks out of the list, ending it and starting a fresh paragraph — usually visible because item 2 restarts at 1. Over-indent past the code-block threshold and your paragraph becomes a code block.

Fenced code blocks (triple backticks) are far more reliable inside lists than indented code blocks, because the fence marks its own boundaries and doesn't compete with list indentation for meaning. See tabs and indentation in markdown for the full picture on when four spaces means code.


Continuing a List After an Interruption

Put a heading, a paragraph, or a horizontal rule at the left margin between two list items and the list ends. The next set of items is a new list, and an ordered one restarts at 1.

There's no markdown syntax to resume a list. Either indent the interruption so it belongs to the preceding item, or use raw HTML with a start attribute where the renderer allows it:

<ol start="4">
  <li>Fourth step</li>
</ol>

If a list genuinely needs a heading in the middle, it's usually two lists that want two headings.


Common Mistakes

  • No space after the marker. -item is literal text. It needs to be - item.
  • No blank line before the list. Some parsers require a blank line between a paragraph and the list that follows it. GFM is lenient here; other parsers are not. Always leave the blank line.
  • Four-space indent for a first-level item. That's a code block, not a list.
  • Mixed markers. Splits one list into several.
  • Trailing spaces for alignment. Two trailing spaces at the end of a line is a hard line break in markdown. Inside a list this produces stray <br> tags.
  • Manually numbering. You can write 1. for every item and markdown numbers them correctly in the output. Useful when you expect to reorder — though it makes the raw source harder to read, so most style guides prefer real numbers.

OpenMark shows both sides at once: the raw markers and indentation with syntax coloring in Markdown view, and the rendered list in Document view. When a nested list quietly becomes a code block, the difference is immediately visible.

For the rest of the syntax, see the markdown cheat sheet or the beginner-oriented how to write markdown. Hyphens also do double duty as horizontal rules and dividers, which is worth knowing before a stray --- under a list item turns into a heading.


Download OpenMark → — $9.99, one-time, native macOS. Nested lists, task lists with clickable checkboxes, and live rendering as you type.