With two cheap tricks I got 147:

  1. Put everything in a li{} block and use the & nesting selector.
  2. Shorten ::before and ::after to :before and :after, as modern browsers support both, apparently.
li{counter-increment:n;&:not(:nth-child(5n)):before{content:counter(n)}&:nth-child(3n):before{content:"Fizz"}&:nth-child(5n):after{content:"Buzz"}}

It could be promising to switch from <ul> to <ol>, but maybe that fizz-, erm, fuzzes with the target too much? :)

104

:nth-child(3n){list-style:"Fizz"}:nth-child(5n){list-style:"Buzz"}:nth-child(15n){list-style:"FizzBuzz"}
data:text/html,<style>:nth-child(3n){list-style:"Fizz"}:nth-child(5n){list-style:"Buzz"}:nth-child(15n){list-style:"FizzBuzz"}</style><ol id=o><script>o.innerHTML='<li>'.repeat(99)</script>

That's an interesting solution. It is worth noting that your solution treats the list markers as part of the output, which the version of the problem in my post avoids. One of the requirements I set out is that all text in the output sequence must be produced directly by the stylesheet. Nevertheless, your solution is an independent and interesting solution to a slightly different version of the stated problem.

You can save two characters using nesting.

li{
  counter-increment: n;
  &:not(:nth-child(5n))::before { content: counter(n) }
  &:nth-child(3n)::before { content: "Fizz" }
  &:nth-child(5n)::after { content: "Buzz" }
}

After minifying this:

li{counter-increment:n;&:not(:nth-child(5n))::before{content:counter(n)}&:nth-child(3n)::before{content:"Fizz"}&:nth-child(5n)::after{content:"Buzz"}}

Also, debatable whether this counts, but browsers don't care if you only use one colon for ::before and ::after so that could be another -3.

Hi,

I think I found a solution that is just ever so slightly shorter (150 characters):

li{counter-increment:n;&:before{content:counter(n)}&:nth-child(5n){&:after{content:"Buzz"}&:before{content:""}}&:nth-child(3n):before{content:"Fizz"}}

Curious if someone will find a significantly shorter solution.

~ luap42