ScreenshotNeo

BlogHTML to image & PDF

How to Fix Inaccurate Table Rendering in jsPDF

Fix jsPDF table width, clipping, pagination, headers, hooks, and version mismatches with practical AutoTable examples and diagnostics.

By the ScreenshotNeo team30 September 20268 min read

How to Fix Inaccurate Table Rendering in jsPDF

Inaccurate jsPDF tables usually come from one of four independent decisions being mixed together: the table’s total width, each column’s width, what happens when text does not fit, and where rows are allowed to break across pages. Fix those decisions separately, then verify that your option names match the installed jspdf-autotable version.

The current AutoTable API is typically used as autoTable(doc, options). Start with explicit margins and deterministic column rules:

import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';

const doc = new jsPDF({ unit: 'mm', format: 'a4' });

autoTable(doc, {
  margin: { top: 20, right: 14, bottom: 18, left: 14 },
  startY: 24,
  tableWidth: 'auto',
  head: [['ID', 'Description', 'Status', 'Amount']],
  body: [
    ['A-100', 'A long description that must wrap inside its cell', 'Paid', '$125.00'],
    ['A-101', 'Short description', 'Pending', '$80.00']
  ],
  styles: {
    fontSize: 9,
    cellPadding: 2,
    overflow: 'linebreak'
  },
  columnStyles: {
    0: { cellWidth: 18 },
    1: { cellWidth: 'auto' },
    2: { cellWidth: 25 },
    3: { cellWidth: 28, halign: 'right' }
  },
  showHead: 'everyPage',
  rowPageBreak: 'avoid',
  pageBreak: 'auto'
});

doc.save('table.pdf');

This example makes the available page width, overflow policy, pagination, and header repetition explicit. If you use another release, first inspect the installed package and its documentation; older examples may use different option names and plugin installation patterns.

1. Confirm the version and invocation

Before changing geometry, record the versions in your lockfile or package manifest:

npm ls jspdf jspdf-autotable

Use the API documented for that version. Current releases expose the autoTable(doc, options) function and use showHead for repeated headers. Older documentation may show showHeader, and copying that name into a newer release can make the setting appear to be ignored. The same issue applies to hook signatures and plugin installation.

For a browser bundle, import both packages explicitly:

import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';

const doc = new jsPDF();
autoTable(doc, { head: [['Column']], body: [['Value']] });
doc.save('example.pdf');

2. Calculate the usable page width

A table is accurate only when its requested width fits the page’s usable width. The usable width is the page width minus the left and right margins. On an A4 document in millimetres, the page is 210 mm wide. With 14 mm margins on both sides, only 182 mm remains.

Table width, overflow, and pagination are separate rendering decisions.
Table width, overflow, and pagination are separate rendering decisions.
Goal Setting What it does
Fill the available page width tableWidth: 'auto' Sizes the table to the page area left after margins.
Fit the content closely tableWidth: 'wrap' Uses content-driven width where possible.
Guarantee geometry tableWidth: 160 Requests a numeric width in the document unit.

Set margins explicitly when a document has a header, footer, or a narrow printable region:

autoTable(doc, {
  margin: { left: 20, right: 20, top: 30, bottom: 25 },
  tableWidth: 'auto',
  head: [['Name', 'Details']],
  body: [['Example', 'Content']]
});

A numeric table width is useful for repeatable templates, but it does not magically make oversized columns fit. Column widths must still add up to a sensible value inside that table width.

3. Set column widths deliberately

tableWidth controls the whole table. cellWidth controls an individual column or cell. They solve different problems. Use columnStyles when a column needs a stable width:

autoTable(doc, {
  tableWidth: 182,
  columnStyles: {
    0: { cellWidth: 22 },
    1: { cellWidth: 74 },
    2: { cellWidth: 42 },
    3: { cellWidth: 44 }
  },
  head: [['Code', 'Description', 'Owner', 'Total']],
  body: rows
});

Use 'auto' when the remaining space should be negotiated, and 'wrap' when content should influence the column’s preferred width. Numeric widths are best when you need a stable report layout. Keep identifiers and monetary columns narrow, then give descriptive text the flexible or widest column.

If a table is wider than the page, reduce padding or font size, wrap text, assign smaller numeric widths, or use horizontal pagination. Allowing the table to run off the page only hides the underlying geometry problem.

4. Choose an overflow policy

Width and overflow are separate decisions. Overflow determines what AutoTable does after text is measured against the cell width.

Overflow Result Use it when
linebreak Wraps text and increases row height. You must preserve the complete value.
ellipsize Truncates with an ellipsis. A compact summary is acceptable.
visible Lets text spill outside its cell. The surrounding layout deliberately permits spillover.
hidden Clips text at the cell boundary. Clipping is an intentional visual rule.
autoTable(doc, {
  styles: { overflow: 'linebreak', fontSize: 8.5, cellPadding: 2 },
  headStyles: { overflow: 'linebreak' },
  columnStyles: {
    0: { cellWidth: 30 },
    1: { cellWidth: 120 }
  },
  head: [['Reference', 'Notes']],
  body: [['REF-001', 'This note wraps onto additional lines rather than being clipped.']]
});

Inspect long headers as well as body cells. A header such as “Subscription renewal date” can be the widest string in a narrow column and make a table look wrong even when body data is short. Long unbroken URLs, hashes, and tracking IDs need special attention because they have few natural break points; give them more width, shorten the displayed value, or choose an intentional clipping policy.

5. Repair vertical pagination

Vertical placement depends on margin, startY, pageBreak, and rowPageBreak. Set startY after preceding content has been drawn:

doc.text('Monthly invoices', 14, 16);

autoTable(doc, {
  startY: 24,
  pageBreak: 'auto',
  rowPageBreak: 'avoid',
  showHead: 'everyPage',
  head: [['Invoice', 'Customer', 'Notes']],
  body: invoiceRows
});
  • pageBreak: 'auto' follows normal flow.
  • pageBreak: 'avoid' moves the table when the whole table can fit later on a page.
  • pageBreak: 'always' starts the table on a new page.
  • rowPageBreak: 'avoid' keeps a row together unless it is taller than a page.
  • showHead: 'everyPage' repeats the header on every table page.

The documented definition of pageBreak describes the behavior when a table spans more than one page. Treat that behavior as an explicit rendering choice rather than an incidental side effect.

6. Split very wide tables horizontally

Some reports contain more columns than any portrait page can hold. Do not squeeze every column until the text becomes unreadable. Enable horizontal pagination and repeat identifier columns:

Horizontal pagination preserves readable columns in wide reports.
Horizontal pagination preserves readable columns in wide reports.
autoTable(doc, {
  horizontalPageBreak: true,
  horizontalPageBreakRepeat: [0],
  horizontalPageBreakBehaviour: 'afterAllRows',
  margin: { left: 12, right: 12 },
  head: [['ID', 'Date', 'Region', 'Product', 'Units', 'Revenue', 'Tax', 'Margin']],
  body: reportRows,
  styles: { fontSize: 8, overflow: 'linebreak' },
  columnStyles: {
    0: { cellWidth: 20 },
    1: { cellWidth: 25 },
    2: { cellWidth: 28 },
    3: { cellWidth: 45 },
    4: { cellWidth: 22 },
    5: { cellWidth: 28 },
    6: { cellWidth: 24 },
    7: { cellWidth: 28 }
  }
});

horizontalPageBreakRepeat keeps an identifier column visible on each horizontal segment. Choose the documented ordering behavior, such as 'immediately' or 'afterAllRows', according to whether readers should see each horizontal segment together or all rows completed before the next segment.

7. Use hooks at the correct stage

Hooks run at different points in AutoTable’s pipeline. Applying a change at the wrong stage can make it disappear when AutoTable applies its own styles.

  • didParseCell: normalize values or styles while cell data is being parsed.
  • willDrawCell: call native jsPDF styling methods immediately before drawing.
  • didDrawCell: add images, borders, or other content after the cell is drawn.
autoTable(doc, {
  head: [['Status', 'Value']],
  body: data,
  didParseCell: ({ cell, section }) => {
    if (section === 'body' && cell.column.index === 0) {
      cell.text = cell.text.map(value => String(value).toUpperCase());
    }
  },
  willDrawCell: ({ cell, section }) => {
    if (section === 'body' && cell.column.index === 0 && cell.text[0] === 'FAILED') {
      doc.setTextColor(180, 0, 0);
    }
  },
  didDrawCell: ({ cell }) => {
    // Draw an icon or other extra shape here, after AutoTable paints the cell.
  }
});

Put content and style normalization in didParseCell, native jsPDF calls in willDrawCell, and post-draw additions in didDrawCell. A style set too early can be overwritten; an image added before the cell background can be covered.

8. Prefer explicit data when HTML parsing is unreliable

HTML import is convenient, but its result depends on the selector, hidden rows and columns, CSS assumptions, and the text AutoTable extracts from each cell. For difficult layouts, pass head, body, and columns directly:

autoTable(doc, {
  columns: [
    { header: 'Account', dataKey: 'account' },
    { header: 'Plan', dataKey: 'plan' },
    { header: 'Notes', dataKey: 'notes' }
  ],
  body: records,
  columnStyles: {
    account: { cellWidth: 34 },
    plan: { cellWidth: 32 },
    notes: { cellWidth: 'auto', overflow: 'linebreak' }
  }
});

With explicit data, you can inspect the exact strings and choose widths before drawing. When using html, verify the selector and test hidden content, colspan or rowspan behavior, and CSS-dependent formatting.

9. Troubleshooting checklist

Symptom Likely cause Fix
Columns run off the page Total requested width exceeds usable page width. Measure margins, use tableWidth: 'auto', reduce numeric widths, or enable horizontal breaks.
Text is cut off Overflow is hidden or a fixed cell is too narrow. Use overflow: 'linebreak', widen the column, or intentionally use ellipsize.
Rows split awkwardly Default row pagination permits splitting. Set rowPageBreak: 'avoid'; handle rows taller than one page separately.
Header appears only once Header repetition is unset or an old option name was copied. Use showHead: 'everyPage' in current releases.
pageBreak appears ignored Version mismatch or the table actually fits. Confirm versions, invocation style, margins, and available height.
Hook style has no effect The hook runs before AutoTable’s drawing styles. Move normalization to didParseCell or native styling to willDrawCell.
HTML table differs from the browser HTML parsing does not reproduce all CSS layout rules. Inspect extracted text and use explicit head, body, and columns.
Long IDs overlap Unbroken strings cannot wrap naturally. Widen the column, transform the display value, or choose clipping deliberately.

10. Validate the rendered PDF

Do not rely only on the source table or a browser preview. Inspect the generated PDF at several boundaries:

  1. The first page, including the area above startY.
  2. A page containing a vertical row split.
  3. The last page, where bottom margins are easiest to miss.
  4. A long unbroken string such as a URL or hash.
  5. The widest column and the longest header.
  6. Every horizontal segment when horizontal pagination is enabled.

Compare the rendered PDF at its intended print or screen size. A table can have mathematically valid widths while still being hard to read because the font, padding, or repeated columns consume too much space.

11. Performance, reliability, and cost considerations

Large tables cost more time and memory because every cell must be measured and drawn. Reduce unnecessary work by preparing strings before rendering, avoiding extremely small fonts that force repeated layout adjustments, and splitting very large reports into intentional sections. If a row contains huge text, cap or summarize it before inserting it into a PDF.

For reliable output, pin compatible package versions, keep a small fixture containing long headers and multi-line values, and inspect page boundaries after dependency upgrades. Option names and hooks can change across major versions, so treat an upgrade as a rendering change that needs visual review.

Or skip the browser setup

If your workflow starts with a web page and ends with a PDF or image, ScreenshotNeo can handle the capture with one GET request. See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Why are my jsPDF columns too wide?

Your column widths and padding exceed the usable page width. Set margins explicitly, use numeric or auto widths, and inspect the total before drawing.

Should I use linebreak or ellipsize?

Use linebreak when the complete value matters. Use ellipsize when a shortened display is acceptable and row height matters more.

Why does my header option do nothing?

Check the package version. Current releases use showHead; older examples may use showHeader.

Can one row always be kept together?

Set rowPageBreak: 'avoid'. A row taller than a full page still cannot fit as one piece.

When should I use horizontal page breaks?

Use them when important columns would become unreadable at a single page width. Repeat identifier columns so each segment remains understandable.