Introduction
Lists are fundamental to web content structure, appearing everywhere from navigation menus and product features to step-by-step instructions and content organization. CSS provides a powerful toolkit for styling list elements, from basic marker customization to advanced counter systems.
This guide covers the complete spectrum of CSS list styling capabilities, helping you create visually appealing and semantically meaningful list-based interfaces for your Next.js projects. Whether you're building navigation menus, documentation sites, or feature comparisons, mastering CSS lists is essential for professional web development.
Essential CSS properties for controlling list appearance
list-style Shorthand
Combine all list properties into a single declaration for concise, maintainable code.
Marker Types
Control marker symbols with disc, circle, square, decimal, roman numerals, and more.
Image Markers
Use custom images or SVG icons as list markers for branded, unique designs.
Position Control
Choose between inside or outside marker placement for precise layout control.
Understanding List Structure
What Makes a List Item
HTML provides three types of lists: unordered lists (<ul>) with bullet markers, ordered lists (<ol>) with numerical markers, and list items (<li>) that contain the actual content. Any element with display: list-item will display a marker, making list styling applicable beyond traditional list elements.
The marker appears in a separate box positioned alongside the list item content. This marker box inherits the text color from the element and follows specific rules for sizing and positioning based on the list-style-position property. Modern browsers handle this box model consistently, but developers should be aware of cross-browser padding differences that can affect indentation.
As documented by MDN Web Docs on CSS Lists and Counters, the list item display type creates both a principal box for the content and a marker box for the bullet or number. Understanding this dual-box model is essential for accurate list styling and layout control.
List Item Marker Anatomy
The marker box is distinct from the principal (content) box, with spacing controlled by padding and margin on the list item element. When using list-style-position: outside (the default), the marker sits in the margin area and the content indents. With list-style-position: inside, the marker becomes part of the inline content flow.
Color inheritance works predictably: markers inherit color from the text of the list item, allowing you to style markers alongside regular text without additional rules. The marker box size is determined by the content and font properties, making it responsive to font size changes on the parent element.
For text direction considerations, list markers automatically adjust for LTR (left-to-right) and RTL (right-to-left) languages. In RTL contexts, the marker position flips appropriately, maintaining proper visual alignment for Arabic, Hebrew, and other RTL scripts.
1/* Unordered list with disc markers */2ul {3 list-style-type: disc;4 list-style-position: outside;5 padding-left: 1.5rem;6 margin: 1rem 0;7}8 9/* Ordered list with decimal markers */10ol {11 list-style-type: decimal;12 list-style-position: outside;13 padding-left: 1.5rem;14 margin: 1rem 0;15}16 17/* Remove all list styling */18.plain-list {19 list-style: none;20 padding-left: 0;21 margin: 0;22}Core List Properties
The list-style Shorthand
The list-style property is a powerful shorthand that combines all list styling properties into a single declaration. It accepts up to three values in any order: list-style-type, list-style-image, and list-style-position.
/* Shorthand syntax */
list-style: <list-style-type> <list-style-image> <list-style-position>;
/* Common examples */
list-style: disc none outside; /* defaults */
list-style: square url('icon.svg') inside;
list-style: decimal inside;
list-style: none; /* removes markers */
According to the MDN list-style property reference, the default value is disc none outside, which explains the familiar filled-circle bullets on unordered lists. Setting list-style: none completely removes markers from list items, which is useful when creating custom list designs or navigation menus.
The shorthand is preferred when setting multiple properties, but individual properties offer more granular control when you only need to change one aspect. The none value can be used for any of the three positions, allowing you to selectively disable markers while keeping image or type settings.
list-style-type: Controlling the Marker Symbol
The list-style-type property controls the marker symbol displayed for each list item. CSS provides extensive predefined marker types to suit different content needs and languages.
Geometric shapes: disc (filled circle, default), circle (hollow circle), square (filled square)
Numerical: decimal (1, 2, 3...), decimal-leading-zero (01, 02, 03...), cjk-decimal (Han decimal numbers for Chinese/Japanese/Korean content)
Alphabetic: lower-alpha/lower-latin (a, b, c...), upper-alpha/upper-latin (A, B, C...)
Roman numerals: lower-roman (i, ii, iii...), upper-roman (I, II, III...)
Language-specific: hebrew, armenian, georgian, hiragana, katakana, arabic-indic, devanagari, bengali, gurmukhi, and many more script-specific options
The complete list of marker types is documented by MDN's list-style-type reference. Using none hides markers entirely, which is commonly done for styled navigation menus where you want complete control over visual presentation.
list-style-image: Using Custom Images
For branded or unique markers, list-style-image allows using an image URL as the marker:
list-style-image: url('images/bullet.svg');
list-style-image: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M5 12l5 5L17 7\"/></svg>');
SVG markers are particularly effective because they scale cleanly and can use currentColor to inherit text colors. If the image fails to load, the browser falls back to the list-style-type automatically. For performance, consider using inline SVG data URIs instead of external files to reduce HTTP requests, especially for pages with multiple lists.
list-style-position: Marker Placement
The list-style-position property controls whether the marker appears inside or outside the principal box. As outlined in the MDN list-style documentation, this affects text wrapping and alignment.
-
outside(default): The marker is positioned in the margin, and the text content indents. This is the traditional bullet-point appearance with clear visual separation. -
inside: The marker becomes the first inline element, and content wraps under the marker. This creates a cleaner left edge for alignment-sensitive designs.
The choice between inside and outside positioning depends on your layout requirements. Outside positioning provides better visual distinction between items, while inside positioning works better when you need a consistent left margin for subsequent text lines.
Advanced Marker Customization with ::marker
The ::marker Pseudo-Element
The ::marker pseudo-element provides direct access to the marker box for granular styling control. This is a significant advancement in CSS that enables marker styling previously only possible with hacks or JavaScript. Browser support is now excellent across Chrome, Firefox, Safari, and Edge.
li::marker {
color: #0066cc; /* Marker color */
font-size: 1.2em; /* Marker size */
font-weight: bold; /* Marker weight */
}
Properties you can style on ::marker:
color- inherits from text by defaultfontproperties - size, weight, family, style, variantcontent- replace or customize marker contentanimationandtransitionproperties for smooth effects
Notably, you cannot set background, border, or layout properties (like margin, padding, display) on ::marker. These limitations are intentional, as markers are a special type of pseudo-element with specific rendering rules.
Replacing Marker Content
The content property on ::marker allows complete replacement of the default marker with custom text or symbols:
li::marker {
content: "→"; /* Custom arrow symbol */
color: #0066cc;
}
li.step::marker {
content: "Step " counter(item) ":"; /* Dynamic numbering with prefix */
}
Combining content with counter() enables dynamic marker content that includes the current counter value. This pattern is powerful for creating numbered steps or custom prefixed labels while maintaining semantic list structure.
Custom Counter Styles with @counter-style
For complete control over marker patterns, the @counter-style at-rule defines custom counter systems. As documented by MDN's CSS Lists guide, this powerful feature allows creating marker styles beyond the predefined options.
@counter-style emoji {
system: cyclic;
symbols: "✅" "🔹" "💡" "✨";
suffix: " ";
}
@counter-style numbered {
system: extends decimal;
prefix: "【";
suffix: "】 ";
}
.list-emoji { list-style-type: emoji; }
.list-numbered { list-style-type: numbered; }
Key @counter-style descriptors:
system- counter algorithm (alphabetic, cyclic, numeric, additive, symbolic, or extends another style)symbols- the counter symbols to use in sequencesuffix- text added after each markerprefix- text added before each markerrange- which counter values the style applies tofallback- style to use when custom style doesn't apply to a specific value
The system: extends option is particularly useful, allowing you to extend an existing style (like decimal) and add custom prefixes or suffixes without defining the entire numbering scheme yourself.
1/* Custom emoji counter style */2@counter-style emoji {3 system: cyclic;4 symbols: "✅" "🔹" "💡" "✨" "📌" "🎯";5 suffix: " ";6}7 8/* Custom numbered style with prefix/suffix */9@counter-style numbered {10 system: extends decimal;11 prefix: "【";12 suffix: "】 ";13}14 15/* Apply custom styles */16.emoji-list { list-style-type: emoji; }17.numbered-list { list-style-type: numbered; }18 19/* Style ::marker pseudo-element */20li::marker {21 color: #0066cc;22 font-size: 1.1em;23}24 25li.highlight::marker {26 color: #e63946;27 font-weight: bold;28}CSS Counters: Beyond List Markers
Understanding CSS Counters
CSS counters enable automatic numbering and counting throughout a document, independent of list elements. This powerful feature is ideal for section headings, figure numbers, footnotes, and any scenario requiring sequential numbering. Counters are maintained across the entire document scope, making them perfect for hierarchical document structures.
Core counter properties:
counter-reset- creates and initializes a countercounter-increment- increases the counter value at specific elementscounter()- displays the current counter valuecounters()- displays nested counter values with separators
counter-reset and counter-increment
The counter-reset property creates a counter and sets its initial value. The default initial value is 0, and counters can be reset to any integer. The counter-increment property specifies how much to increase the counter at each element, with a default increment of 1.
/* Create and initialize counters */
section {
counter-reset: chapter;
}
section h2 {
counter-reset: subsection;
}
/* Increment and display counters */
section h2::before {
counter-increment: chapter;
content: "Chapter " counter(chapter) ": ";
}
section h3::before {
counter-increment: subsection;
content: counter(chapter) "." counter(subsection) " ";
}
Counter scope follows the document tree: a counter reset in a parent element is available to all descendants. This scoping behavior is what makes nested counter hierarchies possible, and it's computed at document composition time rather than during rendering.
counter() and counters() Functions
The counter() function displays the current value of a named counter. An optional second parameter specifies the counter style, allowing you to display counters as roman numerals, letters, or other formats:
/* Simple counter display */
.item::before {
content: counter(item);
}
/* Styled counter output */
.item::before {
content: counter(item, upper-roman);
}
/* Nested counters with separator */
section {
counter-reset: section;
}
h2::before {
counter-increment: section;
content: counters(section, ".", decimal);
}
/* Output: 1., 1.1, 1.1.1, 2., etc. */
The counters() function creates a string representation of nested counters by concatenating counter values at each nesting level. The separator string is placed between values, enabling hierarchical numbering patterns like "1.2.3" automatically. This function is essential for table of contents and document outlines.
Practical Counter Examples
Figure numbering throughout a page:
figure {
counter-increment: figure;
}
figcaption::before {
content: "Figure " counter(figure) ": ";
}
Auto-numbered sections with restart:
article {
counter-reset: section;
}
article h2 {
counter-increment: section;
}
article h2::before {
content: counter(section) ". ";
}
Hierarchical numbering (1, 1.1, 1.1.1):
.chapter {
counter-reset: chapter;
}
.chapter h2 {
counter-reset: subsection;
}
.chapter h3 {
counter-reset: subsubsection;
}
.chapter h2::before { counter-increment: chapter; content: counter(chapter) ". "; }
.chapter h3::before { counter-increment: subsection; content: counter(chapter) "." counter(subsection) ". "; }
.chapter h4::before { counter-increment: subsubsection; content: counter(chapter) "." counter(subsection) "." counter(subsubsection) ". "; }
Best Practices for List Styling
Accessibility Considerations
Proper list styling maintains semantic meaning and ensures accessibility for all users, as emphasized in MDN's Styling Lists guide:
-
Screen readers announce the number of items in a list and identify list structures. Removing
list-style: nonewithout providing visual alternatives can confuse users who rely on these announcements to understand content organization. -
Focus navigation works through list items in document order. Ensure visual focus states are clear with
outlinestyling, and markers don't interfere with interactive elements or click targets. -
Semantic structure matters for assistive technology. Only use list markup (
<ul>,<ol>) for actual lists. Navigation menus should use<nav>with<ul>for semantics, but styling should remove default markers for horizontal layouts. Proper semantic HTML is foundational to accessible web design. -
ARIA roles can supplement list semantics when lists are used for non-standard purposes, but avoid replacing semantic HTML with ARIA. A navigation region with proper landmarks serves screen reader users better than a list with added roles.
Performance Optimization
Efficient list styling contributes to overall page performance, especially for content-heavy pages:
-
SVG markers are more performant than raster images for list markers. Use inline SVG or data URIs to reduce HTTP requests and ensure crisp rendering at any size. SVG markers also support CSS color inheritance through
currentColor. -
CSS containment (
contain: content) on long lists can improve rendering performance by limiting the scope of style and layout calculations to individual list items rather than the entire document. -
Avoid complex counter calculations on frequently repainted elements. Counters are computed once per document composition, but animated properties on
::markercan cause repaints. For marker animations, prefertransformandopacitywhich can be GPU-accelerated. -
Use the shorthand
list-styleproperty when possible. It allows browsers to optimize the declaration parsing and can reduce the overall stylesheet size.
Consistent List Indentation
Browser defaults for list indentation vary, requiring explicit styling for consistent rendering across different browsers and platforms:
/* Cross-browser list reset */
ul, ol {
padding-left: 1.5rem; /* Standardize left padding */
margin: 1rem 0; /* Consistent vertical spacing */
}
/* Nested list adjustments */
ul ul, ul ol, ol ul, ol ol {
margin: 0.5rem 0; /* Tighter spacing for nested lists */
padding-left: 1.5rem; /* Maintain indentation for nested levels */
}
/* Remove default styling when needed */
nav ul {
list-style: none;
padding-left: 0;
margin: 0;
}
Different browsers historically used different default padding values (some used 40px, others 0), so explicitly setting padding-left ensures consistent indentation. When removing markers with list-style: none, also remove padding-left if you don't want the indentation.
Common Use Cases and Patterns
Navigation Menus with Lists
Semantic navigation using list elements with custom styling is a foundational web development pattern. Lists provide inherent semantic meaning for screen readers and maintain logical document structure:
/* Semantic navigation structure */
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
gap: 0.5rem;
}
nav li {
margin: 0;
}
nav a {
display: block;
padding: 0.75rem 1.5rem;
text-decoration: none;
background: #f0f0f0;
border-radius: 6px;
transition: all 0.2s ease;
}
nav a:hover, nav a:focus {
background: #0066cc;
color: white;
}
/* Active state */
nav a[aria-current="page"] {
background: #0066cc;
color: white;
}
Key patterns:
- Remove
list-stylefor horizontal navigation - Use
flexboxfor horizontal layout and alignment - Style
<a>tags, not list items, for larger interactive areas - Maintain semantic
<ul>structure for accessibility - Use
aria-current="page"to indicate the active page
For dropdown menus, nest <ul> elements inside list items and use CSS positioning to reveal submenus on hover. Mobile navigation often requires a different approach, such as collapsing the list into a hamburger menu pattern.
Numbered Instructions and Steps
Step-by-step content with custom numbered styling is common for tutorials, documentation, and process flows:
/* Step counter */
.steps {
counter-reset: step;
}
.step {
position: relative;
padding-left: 3rem;
margin-bottom: 1.5rem;
}
.step::before {
counter-increment: step;
content: counter(step);
position: absolute;
left: 0;
width: 2rem;
height: 2rem;
background: #0066cc;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
Applications: online tutorials, recipe instructions, legal document sections, process documentation, and onboarding checklists. The step counter pattern can be extended with CSS variables for custom colors and sizes to match your design system.
Feature Lists and Product Comparisons
Presenting feature sets with styled list items helps communicate product capabilities clearly:
/* Feature check list */
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
position: relative;
padding-left: 2rem;
margin-bottom: 0.75rem;
}
.feature-list li::before {
content: "✓";
position: absolute;
left: 0;
color: #10b981;
font-weight: bold;
}
/* Feature not included */
.feature-list li.not-included::before {
content: "✕";
color: #ef4444;
}
This pattern scales to responsive grid layouts where feature lists become columns on larger screens. The ::before pseudo-element approach avoids modifying the list-style-image which can have inconsistent fallback behavior.
Table of Contents and Indexes
Auto-generated indexes with CSS counters provide navigation without JavaScript:
/* TOC with hierarchical numbering */
.toc {
counter-reset: toc-section;
}
.toc h2 {
counter-reset: toc-subsection;
}
.toc a::before {
counter-increment: toc-section;
content: counter(toc-section) ". ";
}
.toc h3 a::before {
counter-increment: toc-subsection;
content: counter(toc-section) "." counter(toc-subsection) " ";
}
Table of contents built with CSS counters can link to any page section using standard anchor tags. For longer documents, consider adding smooth scroll behavior via CSS (scroll-behavior: smooth) for a better user experience.