PDFPipe

People and payroll / A4

Payslip HTML template with CSS

A statement of pay for one period: gross made of its parts, deductions made of theirs, the net that was actually paid, and the year to date.

When this document is the right one

Payroll runs. The employee reads it to check one number against last month, and later hands it to a lender or a landlord who needs a document rather than a screenshot.

Why each part is there

The fields are visible in the markup. What is not visible is why they are present, which is the part that gets removed first when someone adapts a template for their own use.

  • Payments and deductions as two parallel columns, because that is how a payslip is read: two lists that must reconcile at the bottom.
  • The basis for each variable line, such as hours and rate, so an unexpected number can be checked rather than queried.
  • Net pay set apart and large, since it is the number that gets compared against the bank account.
  • Year to date figures, which is what the employee actually verifies against and the most common complaint about generated payslips that omit them.
  • Tax code and identification number, because a wrong tax code is the most common cause of a wrong payslip and the employee is the one who spots it.

The template

A complete file. Doctype, stylesheet, body: paste it into a renderer unchanged and it produces a finished A4 page. Replace the sample data and the styling holds.

html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Payslip, May 2026</title>
<style>
  @page { size: A4; margin: 16mm; }
  * { box-sizing: border-box; }
  body { margin: 0; font: 9pt/1.4 Arial, sans-serif; color: #111; }

  header { display: flex; justify-content: space-between; align-items: flex-start;
           border-bottom: 2px solid #111; padding-bottom: 5mm; }
  h1 { margin: 0; font-size: 15pt; }
  .period { text-align: right; }
  .period strong { font-size: 11pt; }

  .employee { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5mm;
              margin: 6mm 0; padding: 4mm 0; border-bottom: 1px solid #ddd; }
  .employee .k { font-size: 7.5pt; text-transform: uppercase; letter-spacing: 0.06em;
                 color: #666; }
  .employee .v { margin-top: 1mm; }

  /* Two columns because a payslip is read as two lists that must agree at the
     bottom: what was earned and what was taken off. */
  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 8mm; }
  h2 { font-size: 8.5pt; text-transform: uppercase; letter-spacing: 0.07em;
       border-bottom: 1.5px solid #111; padding-bottom: 1.5mm; margin: 0 0 2mm; }
  .row { display: flex; justify-content: space-between; padding: 1.6mm 0;
         border-bottom: 1px solid #f0f0f0; }
  .row span:last-child { font-variant-numeric: tabular-nums; }
  .row.sum { border-bottom: 0; border-top: 1.5px solid #111; margin-top: 1.5mm;
             padding-top: 2mm; font-weight: 700; }
  .row small { display: block; color: #777; font-size: 7.5pt; }

  .net { margin-top: 7mm; border: 2px solid #111; padding: 4mm 5mm;
         display: flex; justify-content: space-between; align-items: center;
         break-inside: avoid; }
  .net .label { font-size: 9pt; text-transform: uppercase; letter-spacing: 0.08em; }
  .net .value { font-size: 20pt; font-weight: 700; font-variant-numeric: tabular-nums; }

  /* Year to date is what the employee actually checks against last month.
     Leaving it off is the most common complaint about a generated payslip. */
  .ytd { margin-top: 7mm; break-inside: avoid; }
  .ytd table { width: 100%; border-collapse: collapse; }
  .ytd th, .ytd td { border: 1px solid #ddd; padding: 2mm; text-align: right;
                     font-variant-numeric: tabular-nums; }
  .ytd th { background: #f4f4f4; font-size: 7.5pt; text-transform: uppercase;
            letter-spacing: 0.05em; color: #555; }
  .ytd th:first-child, .ytd td:first-child { text-align: left; }

  footer { margin-top: 8mm; font-size: 7.5pt; color: #666; border-top: 1px solid #ddd;
           padding-top: 3mm; }
</style>
</head>
<body>
  <header>
    <div>
      <h1>Kilnworks Studio Ltd</h1>
      <div style="margin-top:1.5mm">PAYE reference 083/KW44712</div>
    </div>
    <div class="period">
      <strong>Payslip</strong><br>
      Period 2, May 2026<br>
      Paid 28 May 2026
    </div>
  </header>

  <section class="employee">
    <div><div class="k">Employee</div><div class="v">S. Whitlock</div></div>
    <div><div class="k">Payroll number</div><div class="v">KW-0142</div></div>
    <div><div class="k">Tax code</div><div class="v">1257L</div></div>
    <div><div class="k">NI number</div><div class="v">QQ 12 34 56 C</div></div>
  </section>

  <div class="cols">
    <div>
      <h2>Payments</h2>
      <div class="row"><span>Basic salary<small>Annual 38,400, monthly</small></span><span>3,200.00</span></div>
      <div class="row"><span>Overtime<small>6.5 hours at 1.5x</small></span><span>180.29</span></div>
      <div class="row"><span>Studio allowance</span><span>75.00</span></div>
      <div class="row sum"><span>Gross pay</span><span>3,455.29</span></div>
    </div>
    <div>
      <h2>Deductions</h2>
      <div class="row"><span>Income tax (PAYE)</span><span>481.06</span></div>
      <div class="row"><span>National Insurance<small>Category A</small></span><span>236.42</span></div>
      <div class="row"><span>Pension<small>5% qualifying earnings</small></span><span>149.26</span></div>
      <div class="row"><span>Cycle to work<small>11 of 12</small></span><span>62.50</span></div>
      <div class="row sum"><span>Total deductions</span><span>929.24</span></div>
    </div>
  </div>

  <div class="net">
    <div class="label">Net pay, to account ending 4021</div>
    <div class="value">GBP 2,526.05</div>
  </div>

  <section class="ytd">
    <table>
      <tr>
        <th>Year to date</th><th>Gross</th><th>Taxable</th>
        <th>Tax</th><th>NI</th><th>Pension</th>
      </tr>
      <tr>
        <td>2026 to 2027</td><td>6,910.58</td><td>6,611.06</td>
        <td>962.12</td><td>472.84</td><td>298.52</td>
      </tr>
      <tr>
        <td>Employer pension</td><td colspan="5">179.11 this period, 358.22 year to date</td>
      </tr>
    </table>
  </section>

  <footer>
    This payslip is a record of pay and deductions for the period shown. Keep it:
    it is the document a lender, a landlord or a tax enquiry will ask for, and a
    replacement takes several working days. Queries to payroll@kilnworks.example
    within 30 days of the pay date.
  </footer>
</body>
</html>

The layout decision worth understanding

The net pay block is bordered and carries break-inside: avoid. It is one line of content and it is the line the whole document exists to state, so it is given a frame rather than being another row in a table.

The mistake people make adapting it

Leaving off the employer pension contribution because the employee does not receive it. It is part of what the job pays, it appears in every conversation about total reward, and its absence makes the payslip look like it is hiding something it is not.

Rendering it

The page box is declared in the template's own CSS as A4, so the size travels with the markup rather than living in the code that calls the renderer. A4 is also one of the six named formats the API accepts, so it can be passed in the request options instead if that suits your setup better.

bash
curl -X POST https://api.pdfpipe.xyz/v1/pdf \
  -H "Authorization: Bearer $PDFPIPE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<!doctype html>...",
    "options": { "format": "A4" }
  }' \
  --output payslip.pdf

Frequently asked

Can I use this template commercially?

Yes. Copy it, change it, ship it. It is sample markup written to be taken, and the sample data in it is invented, so replace the names and the numbers before anyone sees it.

Why is the page size in the CSS rather than in the API call?

Because a payslip is a A4 document, and that is a property of the document rather than of the code that renders it. Declaring it in the page box means the size cannot be lost between the template and the call, which is the usual way a document ends up on the wrong paper.

Will it look the same in every renderer?

The layout will, because it uses ordinary CSS. The page break behaviour will not: break-inside and break-after are implemented differently by browser engines and by dedicated print engines, and some of the paged media specification is not implemented in browser renders at all. That difference is documented per property in the paged CSS reference.

Other templates

Documents from the same part of a business, and one from each of the other groups.

Paste the template into the playground and get the PDF back. No signup, no key.