It's interesting that most of us reach for margin, padding and/or position properties when faced with a problem like this. I know I did in the past. What if we're looking at it all wrong?
Before I provide a different solution, let's try and understand browser implementations a bit better. If we look at the user agent stylesheet for lists in WebKit, here's what we find:
ul, menu, dir {
//...
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 40px;
}
ol {
//...
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 40px;
}
li {
display: list-item;
text-align: match-parent;
}
The other browsers are similar. The list indent is provided by padding on the ul and ol, not the li. Also worth noting, there's no negative positioning, margin, or indent anywhere to be found. So what is creating that gap between the bullet and the text?
The answer only becomes clear when we try changing the bullet character itself. I'll use the list-style-type property to do this, but instead of giving it a standard value like disc (the definition of which is up to the browser, being a little vague in the specs), I'll give it a quoted string value:
ul {
list-style-type: "•";
}
<ul>
<li>Oh dear</li>
<li>what happened</li>
<li>to the bullet spacing?</li>
</ul>
We can reintroduce the spacing by adding a standard space character after the bullet"
ul.custom-bulleted {
list-style-type: "• ";
}
<ul class="custom-bulleted">
<li>Ah, that's better.</li>
<li>I've never cared for sticky bullets.</li>
</ul>
<ul>
<li>I'm an unstyled list.</li>
<li>I never have that problem!</li>
</ul>
I can only assume that this is akin to what the browsers are doing internally—inserting whitespace after the bullet and essentially floating the whole thing to the immediate left of each li. In fact the specs pretty much confirm this by comparing the default internal implementation to the @counter-style at-rule which allows us to define our own list types:
The following stylesheet fragment provides the normative definition of
these predefined counter styles:
@counter-style disc {
system: cyclic;
symbols: \2022;
/* • */
suffix: " ";
}
The whitespace is right there in the suffix.
There's one unanswered question though… Why does the unstyled list look different to the custom-bulleted class in my example? Again, it seems to come down to each browser's different internal implementation. If you compare them in different browsers, even the custom bullets vary between browsers. What I have found though, is that you can standardise the appearance of a string list-style-type simply by declaring a font-family on li::marker, a feature that wasn't available to us when this question was originally asked.
So, coming back to the original question, if we want to control the space between the bullet and the li, we can do so by changing the width of the whitespace character, or by adding multiple whitespace characters, using a string-based list-style-type, which has wide browser support. Alternatively, we could define a custom list style with @counter-style, but that is not currently supported by Safari.
Unicode offers us a wide range of whitespace characters to choose from. Here's a list that uses an Em Space (U+2003):
ul {
list-style-type: "•\002003";
}
<ul>
<li>I like big gaps and I cannot lie</li>
<li>You other brothers can't deny</li>
</ul>
I know this approach is not the first one that springs to mind. We've been so used to aligning things with margins, padding and indents that we tend to reach for those tools every time. But in many ways, the browser's way makes sense. What could be simpler than indenting the whole list with a single padding declaration on the ul, and letting the bullet take care of itself? (Maybe one day, the li::marker pseudo-element will support padding, for those who reckon they need even more fine-grained control. I won't be holding my breath though.)