PDFPipe

Events, hospitality and membership / A4

Restaurant menu HTML template

A single-sided menu with leader dots between the dish and the price, dietary marks, and an allergen note that promises only what a kitchen can deliver.

When this document is the right one

A menu changes often enough to be printed rather than produced once. The document is read in low light by people who left their glasses behind, which is the constraint that decides every type size on it.

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.

  • A date, because a menu that changes daily and is not dated cannot be checked against a service.
  • Leader dots between dish and price, done with a dotted border and a flex row rather than a run of full stops, so alignment holds whatever the dish is called.
  • A description that says something the name does not, since a description restating the name wastes the only line the dish gets.
  • Dietary marks as short codes with a key, rather than as icons that need a legend to decode.
  • An allergen statement that describes the kitchen honestly instead of promising an absence a small kitchen cannot guarantee.

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>Dinner menu</title>
<style>
  /* A4, single sided, printed daily. The paper is the constraint that decides
     the type size: a menu is read in low light by people who left their
     glasses at home. */
  @page { size: A4; margin: 20mm 22mm; }
  * { box-sizing: border-box; }
  body { margin: 0; font: 10.5pt/1.5 Georgia, "Times New Roman", serif; color: #241f18; }

  header { text-align: center; border-bottom: 1px solid #241f18; padding-bottom: 6mm; }
  .brand { font: 700 10pt/1 Arial, sans-serif; letter-spacing: 0.24em;
           text-transform: uppercase; }
  h1 { margin: 4mm 0 0; font-size: 22pt; font-weight: 400; letter-spacing: 0.04em; }
  .date { margin-top: 2mm; font: 8.5pt Arial, sans-serif; color: #6b6250; }

  h2 { font: 700 8.5pt/1 Arial, sans-serif; letter-spacing: 0.16em; text-transform: uppercase;
       text-align: center; color: #6b6250; margin: 9mm 0 4mm; break-after: avoid; }

  /* Leader dots between the dish and the price, done with a flex row and a
     dotted border rather than with a run of full stops, so the alignment
     holds whatever the dish is called. */
  .dish { margin-bottom: 4mm; break-inside: avoid; }
  .dish .row { display: flex; align-items: baseline; gap: 3mm; }
  .dish .name { font-size: 11.5pt; }
  .dish .dots { flex: 1; border-bottom: 1px dotted #b8ae9a; transform: translateY(-1mm); }
  .dish .price { font-variant-numeric: tabular-nums; }
  .dish .desc { color: #5c5648; font-size: 9.5pt; margin-top: 0.8mm; }
  .dish .marks { font: 7.5pt Arial, sans-serif; color: #8a8171; letter-spacing: 0.06em; }

  .key { margin-top: 9mm; border-top: 1px solid #d8cdb4; padding-top: 4mm;
         font: 8pt/1.4 Arial, sans-serif; color: #6b6250; text-align: center;
         break-inside: avoid; }
</style>
</head>
<body>
  <header>
    <div class="brand">The Brass Kettle</div>
    <h1>Dinner</h1>
    <div class="date">Thursday 14 May 2026</div>
  </header>

  <h2>To start</h2>

  <div class="dish">
    <div class="row"><span class="name">Cured trout, cucumber, dill oil</span>
      <span class="dots"></span><span class="price">11</span></div>
    <div class="desc">Cured in house for two days. Served cold.</div>
    <div class="marks">GF</div>
  </div>

  <div class="dish">
    <div class="row"><span class="name">Roast cauliflower, hazelnut, brown butter</span>
      <span class="dots"></span><span class="price">9</span></div>
    <div class="desc">Vegan without the butter, ask and we will change it.</div>
    <div class="marks">V &middot; N</div>
  </div>

  <div class="dish">
    <div class="row"><span class="name">Bread and cultured butter</span>
      <span class="dots"></span><span class="price">5</span></div>
    <div class="desc">Sourdough baked this morning.</div>
  </div>

  <h2>Mains</h2>

  <div class="dish">
    <div class="row"><span class="name">Hogget shoulder, white beans, salsa verde</span>
      <span class="dots"></span><span class="price">26</span></div>
    <div class="desc">Cooked for eight hours. For two to share, add 14.</div>
  </div>

  <div class="dish">
    <div class="row"><span class="name">Hake, mussels, cider and leek</span>
      <span class="dots"></span><span class="price">24</span></div>
    <div class="desc">Line caught off the Cornish coast. Contains shellfish.</div>
    <div class="marks">GF</div>
  </div>

  <div class="dish">
    <div class="row"><span class="name">Barley risotto, wild garlic, aged cheese</span>
      <span class="dots"></span><span class="price">19</span></div>
    <div class="desc">Wild garlic is nearly over. This dish leaves the menu next week.</div>
    <div class="marks">V</div>
  </div>

  <h2>Puddings</h2>

  <div class="dish">
    <div class="row"><span class="name">Rhubarb, custard, ginger crumb</span>
      <span class="dots"></span><span class="price">9</span></div>
  </div>

  <div class="dish">
    <div class="row"><span class="name">Chocolate and olive oil, sea salt</span>
      <span class="dots"></span><span class="price">9</span></div>
    <div class="marks">V &middot; GF</div>
  </div>

  <div class="key">
    V vegetarian &middot; GF made without gluten-containing ingredients &middot; N contains nuts<br>
    Please tell us about any allergy before you order. Our kitchen is small and we
    cannot guarantee the absence of any allergen, so we would rather have the
    conversation than print a promise we cannot keep.<br>
    A discretionary 12.5% service charge is added to tables of six or more.
  </div>
</body>
</html>

The layout decision worth understanding

The leader dots are a flex child with a dotted bottom border, translated up by a millimetre so they sit on the baseline. Typing a row of full stops looks the same at one size and falls apart at every other, and it cannot be selected or read aloud sensibly by a screen reader.

The mistake people make adapting it

Printing a blanket allergen disclaimer instead of a specific one. A kitchen that says it cannot guarantee the absence of any allergen and invites the conversation is being useful and honest. A kitchen that prints a dish as gluten free and shares a fryer is making a promise that will eventually hurt somebody.

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 menu.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 restaurant menu 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.