HTML Lists & Structured Content Unordered Lists in HTML

Bullet lists in HTML are one of those things that look almost too simple to write an entire article about. But stick with me, because <ul> and <li> are behind a surprising amount of the web. Navigation bars, feature sections, sidebar menus, recipe pages, documentation, tag clouds, all of these rely on the exact same tags you’re about to learn.

By the time you finish this, you’ll know how to create them, nest them, style their markers, customize them completely with CSS, and use them the way real developers do on real projects.

Let’s start.


01What Is an Unordered List in HTML?

An unordered list is a group of items where the order doesn’t change the meaning.

Think about a grocery list. Milk, eggs, bread, butter. It doesn’t matter which one you write first, you’re getting all of them, and none depends on another coming before it. That’s the defining quality of an unordered list: sequence is irrelevant.

Compare that to recipe instructions. “First preheat the oven, then mix the ingredients, then bake.” Swap step one and step three and the recipe breaks. That’s a job for <ol> (ordered list), which we’ll cover in the next article in this series.

In HTML, you create an unordered list with the <ul> tag. Each item inside it uses an <li> tag. The browser automatically adds a bullet point before each item, no CSS needed for the basics.

Simple structure, wide application. Let’s write one.


02Your First Bullet List with ul Tags in HTML

No theory yet. Here’s the code:

<ul>
  <li>HTML</li>
  <li>CSS</li>
  <li>JavaScript</li>
  <li>Python</li>
</ul>

Open that in a browser and you’ll see a clean bullet list with four items. That’s the entire foundation right there.

Structure Anatomy: <ul> and <li>
<ul> Container
<li>
HTML
<li>
CSS
<li>
JavaScript
<li>
Python
<ul> tag
The outer container. Wraps all list items. Renders as a block element.
<li> tag
Each individual item. Can hold text, links, images, or other HTML.

The <ul> tag is the container. Every <li> is one item inside it. You can have as many list items as you need.

Two things worth knowing about how these elements behave: both <ul> and <li> are block-level elements, meaning they each start on a new line and stretch to fill the available width. If you want a refresher on what block-level means, our guide on block and inline elements in HTML covers that clearly.

The <li> tag is also flexible beyond just holding text. It can contain links, images, other HTML elements, or even another list inside it (more on that in a bit).

The Correct Way to Write li Tags in HTML

A mistake beginners sometimes make is placing text directly inside <ul> without wrapping it in an <li> tag. Like this:

<!-- Wrong: text without li -->
<ul>
  HTML
  <li>CSS</li>
  <li>JavaScript</li>
</ul>

<!-- Right: every item in its own li -->
<ul>
  <li>HTML</li>
  <li>CSS</li>
  <li>JavaScript</li>
</ul>

Always wrap every item in an <li> tag. Browsers might still display the wrong version, but it’s invalid HTML and produces unpredictable behavior depending on the browser and context.


03When Should You Actually Use Bullet Lists in HTML?

This is a question a lot of beginners skip, but it matters more than you’d think.

Use <ul> when the order of your items doesn’t affect their meaning. Swap item two and item four. Does it still make sense? If yes, it’s probably an unordered list.

Some clear examples where <ul> is the right choice:

  • Features listed on a product page
  • Ingredients in a recipe
  • Navigation links in a header or sidebar
  • Tags or categories on a blog post
  • A list of supported browsers or devices
  • Required tools or skills for a project

Now, when should you skip <ul>?

When the sequence matters, reach for <ol> instead. Step-by-step instructions, numbered rankings, or any content where “first do this, then do that” is the point all belong in an ordered list.

And don’t use <ul> just to create visual indentation. That’s what CSS margins and padding are for. Using a list tag to push content visually to the right without any actual list meaning is a semantic misuse, and it confuses screen readers.

That brings up something important. HTML elements carry meaning beyond what they look like. Choosing the right element is a core part of what semantic HTML is about. When you write <ul>, you’re communicating to browsers, search engines, and assistive tools that these items form an unordered group. That signal actually matters to machines reading your page.


04Nesting Lists Inside Lists

You can place a list inside another list. This is called nesting, and it’s useful for sub-categories, multi-level menus, or any content that has a natural hierarchy to it.

<ul>
  <li>Frontend
    <ul>
      <li>HTML</li>
      <li>CSS</li>
      <li>JavaScript</li>
    </ul>
  </li>
  <li>Backend
    <ul>
      <li>Node.js</li>
      <li>Python</li>
      <li>PHP</li>
    </ul>
  </li>
  <li>Tools
    <ul>
      <li>Git</li>
      <li>VS Code</li>
    </ul>
  </li>
</ul>
Nested List: Visual Structure
Rendered Output
Frontend

Level 1

HTML

Level 2

CSS

Level 2

JavaScript

Level 2

Backend

Level 1

Node.js

Level 2

Python

Level 2

Depth Legend
Level 1 (Disc)
Direct children of the root <ul>. Browser renders a filled circle.
Level 2 (Circle)
Nested <ul> inside a Level 1 <li>. Browser renders a hollow circle.
Level 3 (Square)
Another <ul> inside a Level 2 <li>. Browser renders a small square.
Browsers switch bullet shapes automatically at each nesting level. You can override this with CSS.

Notice where the inner <ul> goes: inside an <li> element, not directly inside the parent <ul>. That’s the correct structure, and it matters.

Browsers handle the visual indentation automatically and switch bullet styles at each depth level by default: first level is disc, second is circle, third is square. You can override all of this with CSS, which we’ll get to shortly.

The Nesting Mistake That Breaks Your Markup

The most common nesting error looks like this:

<!-- Wrong: nested ul is outside its parent li -->
<ul>
  <li>Frontend</li>
  <ul>
    <li>HTML</li>
    <li>CSS</li>
  </ul>
</ul>

<!-- Right: nested ul lives inside the li -->
<ul>
  <li>Frontend
    <ul>
      <li>HTML</li>
      <li>CSS</li>
    </ul>
  </li>
</ul>

The nested <ul> must sit inside an <li> element, not after the closing </li> tag and not floating directly inside the outer <ul>.

As for how deep to go: two levels is comfortable for most cases, three is manageable. Beyond that, the content itself usually needs reorganizing rather than more nesting. Deep nesting makes content hard to scan, hard to style, and hard to maintain.


05Styling Your List Markers with CSS

The default disc bullet is fine for a plain document. But almost every real project needs something more intentional. CSS gives you good control over this. I know that this is the HTML series but styling the ul and li tags in HTML must require CSS. Don’t worry, you will learn everything about CSS later in the next series if you don’t get it here properly, that’s totally normal, if you want to get an introduction to CSS, you can check out our Introduction to CSS, that will be very useful for you to understand this.

Changing the Marker with list-style-type

The list-style-type property is what you’ll reach for most. It controls what marker appears in front of each list item.

<!DOCTYPE html>
<html>
<head>
  <style>
    .disc-list   { list-style-type: disc; }
    .circle-list { list-style-type: circle; }
    .square-list { list-style-type: square; }
    .none-list   { list-style-type: none; }
    .arrow-list  { list-style-type: "→ "; }
  </style>
</head>
<body>

  <p>disc (default):</p>
  <ul class="disc-list">
    <li>HTML</li>
    <li>CSS</li>
  </ul>

  <p>circle:</p>
  <ul class="circle-list">
    <li>HTML</li>
    <li>CSS</li>
  </ul>

  <p>square:</p>
  <ul class="square-list">
    <li>HTML</li>
    <li>CSS</li>
  </ul>

  <p>none:</p>
  <ul class="none-list">
    <li>HTML</li>
    <li>CSS</li>
  </ul>

  <p>custom string "→ ":</p>
  <ul class="arrow-list">
    <li>HTML</li>
    <li>CSS</li>
  </ul>

</body>
</html>
Interactive: list-style-type Values





  • HTML
  • CSS
  • JavaScript
  • Python
Current CSS
list-style-type: disc;
Filled circle. The browser default for unordered lists.

The most useful values for unordered lists: disc (default filled circle), circle (hollow circle), square (filled square), and none (removes the marker entirely). Modern CSS also lets you pass a string directly as the marker value, like list-style-type: "✓ ";. Browser support for this is solid across all modern browsers.

Bullet Position with list-style-position

The list-style-position property has two values: outside (the default) and inside. It controls whether the bullet sits in the left margin or inside the content area.

<!DOCTYPE html>
<html>
<head>
  <style>
    ul { max-width: 250px; background: #eef2ff; padding: 1rem; }
    .outside { list-style-position: outside; } /* default */
    .inside  { list-style-position: inside; }
  </style>
</head>
<body>

  <p>Outside (default):</p>
  <ul class="outside">
    <li>Bullet sits in the left margin, outside the content box</li>
    <li>Wrapped text starts below the first character</li>
  </ul>

  <p>Inside:</p>
  <ul class="inside">
    <li>Bullet is part of the content box</li>
    <li>Wrapped text aligns below the bullet, not the text start</li>
  </ul>

</body>
</html>
list-style-position: outside vs inside
outside (default)
  • The bullet sits in the left margin area
  • Wrapped lines align to the text start
Most common. Bullet stays in the gutter, text stays clean.
inside
  • The bullet is inside the content box
  • Wrapped lines indent below the bullet
Useful when you want bullets flush with surrounding content.

You’ll mostly stick with the default outside. The inside value comes in handy for compact card layouts or when you need the bullet to align tightly with surrounding content.

The list-style Shorthand

You can combine the type, position, and image into a single property using the list-style shorthand.

<style>
  /* list-style: type  position  image */
  ul.example-1 { list-style: square outside none; }
  ul.example-2 { list-style: circle inside; }
  ul.example-3 { list-style: none; } /* removes everything */
</style>

You don’t need to include all three values. Just include what you want to set, and the browser uses defaults for the rest.


06Removing Bullets and Resetting Browser Defaults

One of the first CSS moves you’ll make with bullet lists in HTML is removing the bullets entirely. This is especially important when building navigation menus or any custom-styled list component.

<style>
  ul {
    list-style: none;  /* removes bullets */
    padding: 0;        /* removes browser default left padding */
    margin: 0;         /* removes browser default margin */
  }
</style>

<ul>
  <li>Item one</li>
  <li>Item two</li>
  <li>Item three</li>
</ul>

Notice the padding: 0 and margin: 0 alongside list-style: none. Browsers add about 40px of left padding to <ul> elements by default, to leave room for the bullet. When you remove the bullets, you almost always want that space gone too.

Forgetting this is one of the most common causes of mysterious spacing issues when building navigation bars or custom list layouts.


07Custom Markers: Going Beyond Default Shapes

When the built-in shapes aren’t enough, the professional move is using the ::before pseudo-element. It gives you full control over everything the marker looks like.

The ::before Method for Complete Control

<!DOCTYPE html>
<html>
<head>
  <style>
    ul.custom-list {
      list-style: none;   /* remove default bullet */
      padding: 0;
      margin: 0;
    }

    ul.custom-list li {
      position: relative;
      padding-left: 1.75rem;  /* space for our custom marker */
      margin-bottom: 0.6rem;
      color: #333;
    }

    ul.custom-list li::before {
      content: "✓";          /* your custom marker */
      position: absolute;
      left: 0;
      color: #6366f1;         /* accent color */
      font-weight: 700;
      font-size: 1rem;
    }
  </style>
</head>
<body>

  <ul class="custom-list">
    <li>Free lifetime updates</li>
    <li>Source code included</li>
    <li>Step-by-step documentation</li>
    <li>Community support access</li>
  </ul>

</body>
</html>
::before Custom Markers in Action
Checkmark
  • HTML
  • CSS
  • JavaScript
content: “✓”;
color: #2ecc71;
Arrow
  • HTML
  • CSS
  • JavaScript
content: “→”;
color: #6366f1;
Pink Dot
  • HTML
  • CSS
  • JavaScript
content: “•”;
color: #ec4899;
Star
  • HTML
  • CSS
  • JavaScript
content: “★”;
color: #f59e0b;

With ::before, you can use any character, emoji, or CSS-generated shape as your marker. You control the color, size, spacing, and alignment down to the pixel. It’s a bit more code than changing list-style-type, but the flexibility makes it the professional choice for anything beyond basic shapes.

There’s also list-style-image, which lets you use an image file as the bullet. In practice it’s rarely used because sizing and positioning are hard to control across browsers. The ::before approach is almost always the better option.


Here’s something that surprises a lot of beginners: the navigation bar on almost every website you’ve visited is built from a <ul>.

If you’ve already worked through our navigation menus in HTML guide, you’ve seen this pattern. But it’s worth showing here in the direct context of what you just learned about unordered lists.

<!DOCTYPE html>
<html>
<head>
  <style>
    nav ul {
      list-style: none;
      padding: 0;
      margin: 0;
      display: flex;
      gap: 2rem;
      background: #141d34;
      padding: 1rem 1.5rem;
      border-radius: 10px;
    }

    nav ul li a {
      color: #cbd5e1;
      text-decoration: none;
      font-size: 0.95rem;
      font-weight: 500;
      transition: color 0.18s;
    }

    nav ul li a:hover {
      color: #6366f1;
    }
  </style>
</head>
<body>

  <nav aria-label="Main navigation">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/tutorials">Tutorials</a></li>
      <li><a href="/about">About</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>

</body>
</html>
Navigation Menu: Before and After CSS

Same HTML structure. Just list-style: none + display: flex transforms a raw bullet list into a proper navigation bar.

Both versions use the exact same HTML. The only difference is CSS. list-style: none removes the bullets. display: flex on the <ul> arranges the items horizontally.

This is the key thing to understand: removing visual bullet styling doesn’t remove the semantic meaning of the list. Screen readers still announce it as a navigation list. Search engines still group those links together. The HTML structure stays meaningful even when the styling makes it look nothing like a traditional list.


09Common Mistakes to Avoid with Bullet Lists in HTML

Let me walk through the ones I see trip up beginners most often.

Placing Content Directly Inside ul

Every item in a <ul> needs to be wrapped in an <li>. Text that sits directly inside <ul> without a parent <li> is invalid HTML. Browsers might render it visually, but the underlying structure is broken and screen readers may skip that content entirely.

Putting a Nested ul Outside Its Parent li

A nested <ul> must go inside an <li> element, not after the closing </li> tag or directly inside the parent <ul>. This breaks the document outline and confuses assistive technologies.

Using Lists for Visual Indentation Only

If your reason for adding a <ul> is to push text to the right visually, stop. Use CSS margin-left or padding-left on the element instead. HTML should describe the structure of your content, not produce visual side effects.

Forgetting to Reset Padding When Removing Bullets

When you write list-style: none, add padding: 0 and margin: 0 right next to it. The browser’s default <ul> padding exists for the bullet. Once you remove the bullet, that space has no purpose and usually causes alignment headaches.

Over-Nesting

Three levels deep is about the practical limit. If you’re going beyond that, it’s almost always a sign that the content itself needs to be reorganized. More nesting makes content harder to scan, harder to style with CSS, and harder to read on mobile screens.


10A Complete Real-World Example

Let’s bring everything together. Here’s a feature list card using custom markers via ::before, a nested category list, and a navigation bar, all from the same foundational tags you learned today.

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      font-family: system-ui, sans-serif;
      background: #0f1729;
      color: #cbd5e1;
      padding: 2rem;
      margin: 0;
    }

    /* ---- Navigation ---- */
    nav ul {
      list-style: none;
      padding: 0;
      margin: 0 0 2.5rem;
      display: flex;
      gap: 0.25rem;
      flex-wrap: wrap;
    }

    nav ul li a {
      display: block;
      color: #94a3b8;
      text-decoration: none;
      padding: 0.5rem 1rem;
      border-radius: 8px;
      font-size: 0.9rem;
      transition: all 0.18s;
    }

    nav ul li a:hover,
    nav ul li.active a {
      background: rgba(99, 102, 241, 0.14);
      color: #818cf8;
    }

    /* ---- Feature Card ---- */
    .feature-card {
      background: #1e293b;
      border: 1px solid rgba(255,255,255,0.07);
      border-radius: 16px;
      padding: 2rem;
      max-width: 460px;
    }

    .feature-card h2 {
      font-size: 1.25rem;
      font-weight: 700;
      color: #fff;
      margin: 0 0 0.5rem;
    }

    .feature-card p {
      font-size: 0.875rem;
      color: #64748b;
      margin: 0 0 1.5rem;
    }

    .feature-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }

    .feature-list li {
      position: relative;
      padding: 0.5rem 0 0.5rem 2rem;
      border-bottom: 1px solid rgba(255,255,255,0.05);
      font-size: 0.9rem;
      color: #cbd5e1;
    }

    .feature-list li:last-child {
      border-bottom: none;
    }

    .feature-list li::before {
      content: "✓";
      position: absolute;
      left: 0;
      top: 0.55rem;
      color: #2ecc71;
      font-weight: 700;
      font-size: 0.9rem;
    }

    /* ---- Category List (Nested) ---- */
    .category-section {
      max-width: 460px;
      margin-top: 2rem;
    }

    .category-section h3 {
      color: #fff;
      font-size: 1rem;
      font-weight: 600;
      margin: 0 0 1rem;
    }

    .category-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }

    .category-list > li {
      background: #1e293b;
      border-radius: 10px;
      padding: 0.75rem 1rem;
      margin-bottom: 0.5rem;
      font-size: 0.9rem;
      color: #94a3b8;
      position: relative;
      padding-left: 2rem;
    }

    .category-list > li::before {
      content: "▸";
      position: absolute;
      left: 0.75rem;
      color: #6366f1;
      font-size: 0.75rem;
      top: 0.875rem;
    }

    .category-list > li .sub-list {
      list-style: disc;
      padding-left: 1.25rem;
      margin: 0.5rem 0 0;
      list-style-type: circle;
    }

    .category-list > li .sub-list li {
      color: #64748b;
      font-size: 0.82rem;
      padding: 0.15rem 0;
    }
  </style>
</head>
<body>

  <!-- Navigation from ul -->
  <nav aria-label="Main navigation">
    <ul>
      <li><a href="#">Home</a></li>
      <li class="active"><a href="#">Tutorials</a></li>
      <li><a href="#">Projects</a></li>
      <li><a href="#">Contact</a></li>
    </ul>
  </nav>

  <!-- Feature Card -->
  <div class="feature-card">
    <h2>What You Get</h2>
    <p>Everything included with your enrollment.</p>

    <ul class="feature-list">
      <li>Free lifetime access to all content</li>
      <li>Full source code for every project</li>
      <li>Step-by-step written documentation</li>
      <li>Works across all modern browsers</li>
      <li>Beginner-friendly, no prior experience needed</li>
    </ul>
  </div>

  <!-- Nested Category List -->
  <div class="category-section">
    <h3>Course Categories</h3>
    <ul class="category-list">
      <li>HTML Foundations
        <ul class="sub-list">
          <li>Document structure</li>
          <li>Semantic elements</li>
          <li>Forms and inputs</li>
        </ul>
      </li>
      <li>CSS Essentials
        <ul class="sub-list">
          <li>Box model</li>
          <li>Flexbox</li>
          <li>Responsive design</li>
        </ul>
      </li>
      <li>JavaScript Basics
        <ul class="sub-list">
          <li>Variables and types</li>
          <li>DOM manipulation</li>
        </ul>
      </li>
    </ul>
  </div>

</body>
</html>
Live Preview: Complete Example

What You Get

Everything included with your enrollment.

  • Free lifetime access to all content
  • Full source code for every project
  • Step-by-step written documentation
  • Works across all modern browsers
  • Beginner-friendly, no prior experience needed
Course Categories
  • HTML Foundations
    • Document structure
    • Semantic elements
    • Forms and inputs
  • CSS Essentials
    • Box model
    • Flexbox
    • Responsive design

Three different uses of <ul> in one page: a navigation menu, a feature list with custom checkmark markers, and a nested category list. Same foundational HTML tags, three completely different looks through CSS alone.


11Quick Reference Cheat Sheet: ul and li Tags in HTML

Cheat Sheet: Unordered Lists in HTML
Tag / Property What It Does Notes
<ul>HTML Creates an unordered (bullet) list Block-level element. Can only have <li> as direct children.
<li>HTML Defines one list item Can contain text, links, images, or nested <ul>.
list-style-typeCSS Controls the bullet marker Values: disc, circle, square, none, or any string like “✓ “.
list-style-positionCSS Places bullet inside or outside content box Values: outside (default) or inside.
list-styleCSS Shorthand for type, position, and image Use list-style: none to remove all markers.
list-style-imageCSS Uses a custom image as the bullet Limited control. Prefer ::before for custom designs.
::beforeCSS Creates a fully custom marker Set list-style: none first, then add content via ::before.
padding: 0; margin: 0;CSS Resets browser defaults on <ul> Always include when removing bullets for nav menus or custom lists.
Nesting rule Put nested <ul> inside <li> Never place a nested <ul> directly inside a parent <ul>.

12What You Learned Today

You now have a solid, complete understanding of bullet lists in HTML.

You know what the <ul> tag is and when to use it. You know how to write <li> tags correctly, how to nest lists inside other lists, how to style markers with list-style-type, how to reset browser defaults, and how to build fully custom markers using ::before.

You also saw something that a lot of beginners don’t realize until much later: navigation menus are just styled unordered lists. That connection alone will change how you read other people’s code going forward.

Next up in this series: Ordered Lists. Numbers, letters, roman numerals, and how to control the counting behavior with attributes like start, type, and reversed. The <li> tag works exactly the same, but there’s a lot of useful power in how ordered lists handle sequencing.

You’ve built a real foundation here. Keep going.

Unordered Lists in HTML

Choose a difficulty to load your challenge

Loading challenge…

Ready to Practice?

Select a difficulty level above to load your challenge.

Easy

1
Live Preview
Great Work!