#2947·pdfmake

Table cell background fills inside an unbreakable block are painted in reverse order

Author: alextbokCreated Aug 30, 2026Updated Aug 30, 2026

Since 0.3.0, table cell background fills that are drawn inside an unbreakable block (dontBreakRows: true rows, unbreakable stacks) are painted in reverse of the order in which they were drawn. When those fills overlap, the wrong one ends up on top.

The case that surfaces this in practice is an outer table with a layout.fillColor (zebra striping) whose rows use dontBreakRows: true and contain a nested table with its own cell fillColor: the outer row fill paints over the nested cell fill. The nested table's borders and text still render correctly, since those are not background-band items, so the result is a bordered box with the wrong fill colour.

Minimal example (runnable on the playground)

javascript
{
  content: [{
    table: {
      dontBreakRows: true,
      widths: ['*', 'auto'],
      body: [[
        { text: 'row with zebra fill' },
        { table: { widths: ['auto'], body: [[{ text: 'X', fillColor: '#1C488A', color: '#FFFFFF' }]] } },
      ]],
    },
    layout: { fillColor: () => '#EAF1FC' },
  }],
}

Expected: the nested cell is a dark blue (#1C488A) box with a white X, sitting on the light blue (#EAF1FC) row fill. This is what 0.2.12 produces.

Actual (0.3.x, including current master): the nested cell's dark blue fill is covered by the light blue row fill, so the box appears light blue and the white X is almost invisible against it.

Removing dontBreakRows: true makes it render correctly, which is consistent with the fills only being reordered when they pass through an unbreakable block.

Root cause

TableProcessor marks cell fill vectors drawn while a transaction is open with _isFillColorFromUnbreakable. On commit, ElementWriter.addFragment splices each marked fill into the page's background band so that lines and text paint above it:

javascript
const endOfBackgroundItemsIndex = ctx.backgroundLength[ctx.page];
page.items.splice(endOfBackgroundItemsIndex, 0, { type: 'vector', item: v });

The index is fixed for every fill in the block, so each successive splice inserts the new fill in front of the previously inserted one and the block's fills end up in reverse draw order. In 0.2.x addFragment pushed the block's items in order, which preserved it.

Versions: correct in 0.2.12, wrong in 0.3.x up to and including 0.3.11 and current master.

A pull request follows.