D
P
0

HTML & CSS

CV Bullets Pile Up at the Bottom of the PDF and Fail ATS Parsing? A `li::before` Hack Chrome Paints in a Late Pass

July 27, 2026·4 min read
CV Bullets Pile Up at the Bottom of the PDF and Fail ATS Parsing? A `li::before` Hack Chrome Paints in a Late Pass

My CV is not a Word file. It is HTML plus CSS rendered to PDF through headless Chrome with print-to-PDF. There are six variants, three styles across two languages, and every one of them passed my eyeball review: clean fonts, aligned bullets, firm section headings. While tailoring one variant for a job application, I ran pdftotext against the resulting PDF on a whim, just to see the text a machine actually reads. What came back made me stop typing.

Every bullet, the that sits neatly in front of each experience line on screen, lined up at the very bottom of the PDF text stream, after the Education section. Not shifted a little, but all of them collected into a row of orphan glyphs at the end of the document. An ATS that reads a PDF by stream order rather than visual position will glue all of that bullet content onto the wrong section, or treat it as noise.

There was a second symptom, no less nasty. Section headings like Professional Experience came out of pdftotext as broken glyph runs: P RO F ES S I O N A L. A human can still read it, but an ATS matching the keyword "Professional Experience" will never match a string with random spaces wedged into the middle.

Tracing it

My first guess was the font. Maybe font subsetting scrambled the glyph order as Chrome wrote the PDF. I swapped to a plain system font, re-rendered, and got the exact same result. Bullets still dropped to the bottom.

Second guess: maybe this was just a pdftotext quirk. I tried a different extractor, then opened the PDF in a viewer, selected the text with the mouse, and pasted it into an editor. Same order every time: bullets stacked at the end. So it was not a tooling bug. It was a bug baked into the PDF content itself.

Only then did I stop guessing and isolate. I built a minimal HTML page with a single bulleted list, rendered it, and checked the stream. The bullets from li::before landed at the bottom. Then I deleted the ::before rule and used the native list marker, rendered again, and the bullets snapped back onto their lines. That is where the root cause showed itself.

Why it happens

The bullets in my CV used a common trick that makes bullet spacing and color easy to control:

li {
  position: relative;
  list-style: none;
}
li::before {
  content: "•";
  position: absolute;
  left: 0;
}

The problem is that ::before is generated content, and here it lives on an absolutely positioned element. Chrome print-to-PDF paints generated content in a later paint phase than normal-flow text. Because the bullet is absolutely positioned, its visual coordinates stay correct on screen, so the eye never suspects anything. But the order glyphs get written into the PDF content stream follows paint order, not visual order. So all the glyphs get emitted after the entire list text, which in my layout happened to sit after the Education section. A stream-order parser reads exactly that order.

The heading issue has a different cause from the same family. I set letter-spacing: 1.4px on the section h2 to make it feel airy and deliberate. With tracking that wide, Chrome positions each glyph individually with a sizable gap. pdftotext sees those large inter-glyph gaps and inserts spaces between them, so Professional turns into P RO F ES S I O N A L.

The fix

For the bullets, drop the ::before trick and go back to the native list marker:

li {
  list-style: disc;
  padding-left: 14px;
}

Native list-style markers are emitted inline with their list item in the stream, in the right order. The bullets return to their lines in the extracted text immediately.

For the headings, cut letter-spacing from 1.4px to 0.3px:

h2 {
  letter-spacing: 0.3px;
}

At 0.3px the inter-glyph gaps are small enough that pdftotext keeps the letters as one word. The heading still reads airy visually, but the machine sees a single string.

Always verify against the text a machine actually reads, not the render:

pdftotext cv.pdf - | less

Now the bullets sit on their lines and headings read as whole words. One important note: I only fixed the newest variant. The six older CV sources still carry both bugs, so I flagged them to fix before their next use. One broken template quietly breaks every descendant.

Checklist

  • Do not trust the visual PDF. Test with pdftotext to see the stream order an ATS actually reads.
  • Avoid li::before for bullets in any document you will parse. Chrome paints generated content in a late pass, so bullets migrate to the bottom of the stream.
  • Use native list-style: disc so the marker is emitted inline with its line.
  • Wide letter-spacing like 1.4px fragments headings into separate glyphs in pdftotext. Bring it down to around 0.3px.
  • If you keep many document variants from one template, fix all of them, not just the one you are using right now.