Styling List Markers Using the CSS ::marker Pseudo-Element

The ::marker pseudo-element targets the bullet or number box of a list item, allowing independent styling of markers without extra markup.

The common wrong assumption about the CSS ::marker pseudo-element is that you must wrap list item text in a span to style the bullet or number separately from the text. That pattern adds markup, breaks when the list item is generated dynamically, and fails to address the marker box directly. What is true: ::marker targets the marker box of a list item, letting you change its color, font, size, and even its generated content without touching the HTML. ::marker has a specificity of (0,0,1), exactly the same as a single type selector, so it competes on equal footing with a rule like li { color: gray; }. You only need to know the limited set of CSS that actually works on it.

The marker box is the part of the list item that holds the bullet or number. For an <li> or any element with display: list-item, the browser generates this box as a child of the list item, and ::marker selects it. The marker box does not inherit most declarations from the list item; it has its own inheritance chain, picking up only text-combine-upright, unicode-bidi, direction, color, content, and all font and animation values. That means setting font-family on the <li> does cascade to the marker, but setting padding on the <li> does not affect the marker box at all.

The declarations you can set on ::marker are deliberately narrow. Per the CSS Pseudo-Elements Module Level 4 specification, the allowed set is color, font-family, font-size, font-weight, font-style, font-variant, text-combine-upright, unicode-bidi, direction, content, text-transform, white-space, and the animation values that correspond to those. You cannot set display, padding, margin, border, background, width, height, or any box-model declaration on ::marker. Attempting to do so is a common mistake; the declaration is ignored by the browser. The marker box is not a flex or grid container. It is a small inline box that holds the generated text or image.

Styling List Markers Using the CSS ::marker Pseudo-Element

To style a list marker independently from the list item’s text, write a rule that ends with ::marker. The color declaration is the most straightforward: it changes only the bullet or number, leaving the text of the <li> untouched. This works because color is inherited by the marker box from the list item, but a direct ::marker rule overrides that inheritance. The specificity of (0,0,1) means li::marker has a specificity of (0,0,2), while a bare ::marker rule has (0,0,1); both are lower than a class selector’s (0,1,0), so if you have a class like .warning on the list item, that class’s color declaration will win unless your ::marker rule has higher specificity or comes later in the cascade.

The font-family descriptor on ::marker lets you set the font for the bullet or number independently from the list item’s text. Use this when you want a serif number in a sans-serif list, or a monospace bullet in a proportional body. font-size also works, so you can make the marker larger or smaller than the text without affecting the line height of the list item. Because the marker box is inline, changing font-size will not change the spacing between list items; the line box grows to accommodate the marker, but the marker does not push the text down.

Here is a minimal example that styles the bullet color and size while leaving the text alone:

li::marker {
  color: #c00;
  font-size: 1.25em;
  font-weight: bold;
}

This rule targets every list item’s marker box, turning the bullet or number red, larger, and bold. The text inside the <li> remains the default color and weight because no rule targets it directly. If you want the marker to inherit the list item’s font but change only the color, write li::marker { color: inherit; }; that is not the default, because the marker’s color is inherited from the <li> already, and inherit makes the intent explicit.

Custom Bullet Points CSS

To get custom bullet points, use the content declaration on ::marker. content replaces the default marker text, which is generated from the list-style-type value. The initial value of content for ::marker is normal, meaning the browser uses the list item’s list-style-type to generate the bullet or number. When you set content to a string, an image, a counter, or a quoted string, you take over the marker entirely.

For a simple custom bullet, set content to a string:

ul li::marker {
  content: "➤ ";
  color: #07c;
}

This replaces the default disc with a right-pointing arrow followed by a space, in blue. The space after the arrow is part of the content string, so it separates the marker from the text. The color declaration applies to the arrow because the marker box is generated as text. You can use any Unicode character in the string, including emoji, but multi-character strings display as a single unit without wrapping.

For images, content accepts an <image> value, such as url(arrow.svg) or a gradient. This replaces the old list-style-image technique, which had limited control over size and alignment. With ::marker, you can combine an image with a color fallback:

li::marker {
  content: url("bullet.svg");
  font-size: 1em;
}

The image size is controlled by the font-size of the marker, because the image is treated as an inline replaced element within the marker box. Set font-size: 1em and size the SVG’s intrinsic dimensions accordingly. If the image fails to load, the browser falls back to the list-style-type value, so set that to a sensible default.

Style List Marker CSS

Changing Case and Weight

Style list marker CSS goes beyond simple color and content changes. Use text-transform to change the case of a letter-based marker. For example, if you have an ordered list with lowercase letters from list-style-type: lower-alpha, force the marker to uppercase:

ol li::marker {
  text-transform: uppercase;
  font-weight: bold;
}

This rule does not change the list item’s text; it only affects the generated letter. font-weight is allowed on ::marker, so the letter becomes bolder than the text if the text has a normal weight. You can also set font-style: italic to slant the marker independently.

Animating the Marker

The animation declarations for ::marker include transition and animation on the allowed set. You can transition the color of the marker on hover, but you cannot transition content because it is not an animatable property. font-size can transition, but it causes layout changes to the line box, so it may be janky on a long list. Animating color is the safest and most useful effect.

Suppressing the Marker Box

One failure case: if you set content to none on ::marker, the marker box is not generated at all. This is different from list-style-type: none, which removes the marker but still reserves space in some engines. With content: none, the list item has no marker, and the text starts at the left edge of the list item. This can be useful when you want a list without bullets but still want the semantic `

    ` element. Be aware that it removes the marker entirely rather than hiding it visually.

::marker Content Property

The content declaration on ::marker is the core of custom numbering and bullets. The allowed values are normal, none, <string>, <image>, <counter>, open-quote, and close-quote. normal means the browser generates the default marker from list-style-type. none suppresses the marker. A <string> is any quoted text, including an empty string "" to hide the marker while keeping the box. An <image> is a url() or a gradient. A <counter> is a counter reference, such as counter(item) or counters(item, ".").

Custom Numbering with Counters

For custom numbering in an ordered list, use the counters() function. This function takes a counter name and a separator string, and it can include a style for each level. The classic example is a nested list where each level increments the same counter, and the marker shows the combined value with a dot separator:

ol {
  counter-reset: item;
}

ol li {
  counter-increment: item;
}

ol li::marker {
  content: counters(item, ".") " ";
  font-weight: bold;
}

This rule sets up a counter named item, increments it for each li, and uses counters(item, ".") to generate a string like 1, 1.1, 1.1.1 depending on nesting depth. The trailing space in the content string separates the number from the list item text. Change the style of the counter by adding a style keyword, such as counters(item, ".", decimal) or counters(item, ".", lower-roman).

Quotation Marks as Bullets

The open-quote and close-quote values are rarely used on markers, but they let you use the same quotation marks as the rest of the document. This is useful if you want a quotation mark as a bullet and you want it to respect the quotes property on the root element. Most authors use a string literal instead, because the quotes property is not widely customized.

List-Style-Type Alternatives

Why ::marker Replaces the Old Approach

List-style-type alternatives are the reason ::marker exists. The old way to customize a bullet was list-style-type, which accepts a set of predefined keywords like disc, circle, square, decimal, lower-alpha, and upper-roman. That works for simple cases, but it cannot produce a custom string, an image, or a dynamic counter. The list-style-image property was the only image option, and it had no way to control the size or alignment of the image without affecting the list item’s line box.

Layering a Fallback

::marker replaces all of those for most use cases. Set list-style-type as a fallback, because the content declaration on ::marker takes precedence when it is not normal. This is a good practice: set list-style-type: disc on the `

  • ` and then `content: "• "` on `::marker` for a bullet that is guaranteed to render even if the browser does not support `::marker`. In modern browsers, the `::marker` rule wins, and older browsers fall back to the disc.

  • Avoid Background-Image Hacks

    A common mistake is to set list-style-type: none and then try to add a custom bullet via background-image on the `

  • `. That technique requires adding padding to the list item to make space for the background, and it breaks when the text wraps because the background repeats or is clipped. With `::marker`, you avoid all of that: the marker box is part of the list item's layout, so the text wraps around the marker naturally.

  • One more alternative is to use a flex or grid container on the list item and place the marker manually with a pseudo-element on a child. That is a heavier approach because it requires changing the <li> from display: list-item to a flex container, which removes the automatic marker generation. The ::marker approach keeps the list semantic and the accessibility tree intact. It is the preferred method for custom bullets.

    FAQ: The Five Questions That Matter

    Image Sizing and Element Scope

    Can I change the size of a custom image bullet with ::marker?

    Set the font-size on the ::marker rule. The image is scaled relative to the font-size, because the marker box is an inline box. If your SVG has a width of 1em, it will match the font size. If it has a fixed pixel size, it will not scale; resize the SVG file instead.

    Does ::marker work on elements other than <li>?

    Any element with display: list-item can have a ::marker. This includes <li>, <summary> in a <details> element, and any element where you manually set display: list-item via CSS. For example, div { display: list-item; list-style-type: disc; } will generate a marker that div::marker can style.

    Long Strings and Hidden Elements

    What happens if I set content to a very long string?

    The marker box will contain the full string, but it will not wrap; it will overflow or be clipped depending on the list item’s overflow property. Keep the marker content short, like a single character or a short word, because the marker box is not a flexible container.

    Can I use ::marker with display: none on the list item?

    No. If the list item is display: none, the marker is not generated, and any ::marker rule has no effect. The marker box only exists when the list item participates in layout.

    ::marker vs ::before

    How is ::marker different from ::before on the list item?

    ::before is a child of the list item and participates in the list item’s flex or block layout. It is not the marker box. ::before can have display, padding, background, and other box properties, while ::marker cannot. If you need a marker with a background or border, use ::before instead, but be aware that it changes the accessibility tree differently.

    Browser Support and the Interop Gap

    Baseline Availability

    The ::marker pseudo-element is Baseline widely available, according to the Baseline status grouping that tracks web platform features across Blink, WebKit, and Gecko engines. Chrome shipped it, Edge shipped it, Firefox shipped it, and Safari shipped it. The feature is present in all modern browsers, but the interop gap is not zero.

    The Safari Animation Gap

    Safari shipped ::marker later than Blink and Gecko, and it has restrictions on which properties animate. Safari does not animate font-size or font-weight on ::marker, even though those are in the allowed set. The Interop project has tracked this gap, and the WebKit Feature Status page lists ::marker as supported, but the animation gap persists. If you need to animate the marker, stick to color. opacity itself is not in the allowed set for ::marker, so you cannot use it. The only safe animation is color, which transitions in all engines.

    Testing and Detection

    The Interop project includes ::marker in its test suite, and the number of failing WPT subtests has dropped significantly. The remaining failures are in the animation domain in Safari. The practical advice: test your ::marker styling in Safari, especially if you animate color or change font values on hover. content is not animatable in any engine, so do not try to transition it.

    To detect support for ::marker, use an @supports rule with a selector test:

    @supports selector(li::marker) {
      li::marker {
        content: "\2022 ";
        color: #333;
      }
    }
    

    The selector() function in @supports is not supported in all browsers, so this test may fail in older Safari versions. A more robust fallback is to set both list-style-type and ::marker rules; the ::marker rule overrides when supported, and list-style-type is used otherwise. That is the recommended approach for production code.

    Specificity and the Cascade

    How Specificity Works

    The specificity of ::marker is exactly (0,0,1), the same as a type selector. Because pseudo-elements are considered type-like in the cascade, a rule like li::marker has specificity (0,0,2), while ul li::marker has (0,0,3). A class selector like .custom-list has specificity (0,1,0), which beats any number of type or pseudo-element selectors. If you have a class on the `

  • ` and you want the marker to override a class-based color, write `li.custom-list::marker` to get (0,1,1) and beat a plain `.custom-list` rule.

  • Inheritance and Nesting

    The content declaration is not inherited, but it has an initial value of normal. If you set content on ::marker for an ancestor, it does not apply to nested list items unless you target them explicitly. Each list item’s marker box is a separate pseudo-element, and content is computed on the element that generates the marker. In a nested list, the inner `

  • ` has its own `::marker`, and it ignores the outer `::marker` rule unless the selector matches it.

  • Keeping the Default Number

    When you set content on ::marker, you lose the automatic counter value from list-style-type. To keep the default number, use content: counter(list-item) explicitly. The list-item counter is the built-in counter that tracks the position of the list item. For ordered lists, counter(list-item) returns the current number, and you can add a style like counter(list-item, decimal) or counter(list-item, lower-roman).

    A common mistake: setting content: ") " on an ordered list’s ::marker to add a parenthesis after the number. You need to include the counter: content: counter(list-item) ") ". Without the counter, you replace the number with a closing parenthesis. The same applies to custom counters; always reference the counter you want to display.

    The Fallback Technique and Failure Cases

    Building a Robust Fallback

    The fallback technique for browsers that do not support ::marker is to style the `

  • ` element directly. Set `list-style-type` to a sensible default and accept that you cannot customize the marker in those browsers. No current browser lacks `::marker` support, but older device-locked browsers like iOS Safari on an old iPhone may not have it. To cover that gap, write a base rule for `list-style-type` and then layer `::marker` on top.

  • Box Properties Are Ignored

    The failure case for ::marker is when you try to use box properties. If you set background or padding on ::marker, the browser ignores it, and your custom bullet looks unchanged. The spec explicitly forbids these, so there is no workaround within ::marker. If you need a marker with a colored box or a border, use the ::before pseudo-element on the list item instead, and set list-style-type: none to remove the default marker. That technique is well documented, but it changes the accessibility tree because ::before content is exposed differently than the marker box.

    Inheritance Boundaries

    Another failure case is expecting declarations on ::marker to inherit to the list item’s text. The color on ::marker only affects the marker box; it does not cascade to the `

  • ` text. If you set `li::marker { color: red; }`, the list item's text stays its default color. This is by design; the marker is a separate box. To make both the marker and the text red, set `color` on the `
  • ` and let the marker inherit it, or set both explicitly.
  • The text-transform declaration on ::marker applies to the marker box, not the marker text. If you set text-transform: uppercase on ::marker for a list with content: counter(list-item), the number does not change because numbers have no case. If you use a letter counter like lower-alpha, the letter becomes uppercase. text-transform is scoped to the marker; it will not affect the list item’s text.

    Practical Code Samples for Real Projects

    Unordered List with Hover Effect

    Here is a complete example that styles an unordered list with custom arrow bullets and a hover effect:

    <ul style="list-style-type: disc; padding-left: 1.5em;">
      <li>First item</li>
      <li>Second item</li>
      <li>Third item</li>
    </ul>
    
    li::marker {
      content: "\2192 "; /* right arrow */
      color: #007acc;
      font-family: "Segoe UI", sans-serif;
      font-size: 0.9em;
      transition: color 0.2s ease;
    }
    li:hover::marker {
      color: #e60000;
    }
    

    This uses a disc as the fallback list-style-type, and the ::marker rule replaces it with an arrow in all engines that support ::marker. The hover transition animates the color, which works in all engines. The arrow is followed by a space in the content string, so the text is separated from the marker.

    Ordered List with Bracket Counters

    For an ordered list with custom numbering, here is a sample that uses the built-in list-item counter:

    ol {
      list-style-type: none;
      counter-reset: my-counter;
    }
    
    ol li {
      counter-increment: my-counter;
    }
    
    ol li::marker {
      content: "[" counter(my-counter) "] ";
      font-family: "Courier New", monospace;
      font-size: 0.8em;
      color: #666;
    }
    

    This replaces the default decimal number with a bracket-enclosed counter, styled in monospace. list-style-type: none is a fallback for browsers without ::marker support; in that case, the list has no visible marker, but the content is still in the HTML. For a fallback number, use list-style-type: decimal instead, which will show the default number in old browsers.

    Nested List with Dotted Hierarchy

    A more advanced sample uses counters() for a nested list with a dotted hierarchy:

    ol {
      counter-reset: section;
      list-style-type: none;
    }
    
    ol li {
      counter-increment: section;
    }
    
    ol li::marker {
      content: counters(section, ".") " ";
      font-weight: bold;
    }
    
    ol li li::marker {
      content: counters(section, ".") " ";
      font-weight: normal;
    }
    

    The outer list items show 1, 2, 3, and nested items show 1.1, 1.2, and so on. The nested ::marker rule resets the font weight to normal, so only the top-level numbers are bold. The counter is shared across all levels because it is reset on the ol and incremented on every li. The counters() function joins the values with a dot, producing the hierarchical number.

    What Not to Do: Common Mistakes

    Box-Model Properties

    The most common mistake is attempting to change display, position, or box-model declarations on ::marker. The specification does not allow these, and browsers silently ignore them. You cannot make the marker display: block, you cannot position: absolute, and you cannot set margin or padding. If you find yourself needing those, step back and reconsider the approach; the marker box is meant to be a simple inline replacement.

    Inheritance Confusion

    The second common mistake is expecting declarations set on ::marker to inherit to the list item’s text. The color, font-size, and font-family on ::marker only affect the marker box. The list item’s text is a separate inline box, and it inherits from the `

  • ` element, not from the marker. To style both, write a rule for the `
  • ` and a separate rule for `::marker`.
  • Forgetting to Set Content

    The third mistake is using content: normal or content: none without understanding the difference. normal keeps the default marker, and none removes it. If you want a custom bullet but forget to set content, the ::marker rule has no effect beyond the declarations you set, and the default list-style-type still applies. Always set content explicitly when you want a custom bullet.

    Missing display: list-item

    The fourth mistake is assuming ::marker works on elements without display: list-item. A `

    ` with no list styling will not have a marker box, and `div::marker` will not match anything. You must set `display: list-item` and a `list-style-type` on the element for the marker to be generated.

    The One Thing to Do Next

    Open your browser’s developer tools, inspect a simple <ul> with a few <li> elements, and add a rule for li::marker { content: "\2022 "; color: #333; } in the styles pane. Then change the content to a different character, like a checkmark or an arrow, and watch the marker update instantly. This hands-on experiment will teach you the boundaries of the marker box faster than reading the spec, and it will show you exactly which declarations are honored and which are ignored. From there, apply the same technique to an ordered list with counter(list-item) to see the numbering change style without touching the HTML.