Build a Custom Template
This guide walks you through building a fully customized invoice template. You start from an existing template, then reshape it to match your exact requirements — header, content, footer, CSS styling, and translations.
Starting Point: Duplicate an Existing Template
The most efficient way to create a custom template is to duplicate one of the built-in designs and modify it:
- Go to the Templates section
- Find a template that is closest to what you want
- Click Duplicate to create your own copy
- Rename it (e.g., "My Custom Invoice")
- Open it in the Template IDE
This gives you a working foundation with all sections, variables, and translations already in place. You can then modify any part — or replace entire sections — without starting from zero.
docs101 does not offer a blank template option, and for good reason: starting from a working design is faster and less error-prone. You get correct variable references, complete translation keys, and a proven CSS structure from the start.
Need a completely custom design? If you have an existing PDF or design mockup that you want to turn into a docs101 template, contact us — we can convert it into a private or public template for you.
Recommended Structure
A professional invoice template typically follows this layout:
+---------------------------+
| HEADER | Logo, company name, address
+---------------------------+
| INVOICE METADATA | Invoice number, date, due date
+---------------------------+
| CUSTOMER SECTION | Bill-to name, address
+---------------------------+
| LINE ITEMS TABLE | Positions with qty, price, total
+---------------------------+
| TOTALS SECTION | Subtotal, VAT, grand total
+---------------------------+
| PAYMENT INFORMATION | Bank details, payment terms
+---------------------------+
| FOOTER | Legal notice, company info
+---------------------------+
Step 1: Build the Header
The header appears at the top of every page. Click the Header tab in the Template IDE.
Header HTML
<div class="header-container">
<div class="header-top">
<div class="company-branding">
<img src="{{ company.logo_url }}" alt="Logo" class="company-logo">
<h1 class="company-name">{{ company.name }}</h1>
</div>
</div>
<div class="invoice-title-section">
<h2 class="invoice-title">{{ t('invoice_title') }}</h2>
</div>
<div class="invoice-meta">
<div class="meta-item">
<span class="label">{{ t('invoice_number_label') }}</span>
<span class="value">{{ invoice.invoice_number }}</span>
</div>
<div class="meta-item">
<span class="label">{{ t('date_label') }}</span>
<span class="value">{{ invoice.invoice_date }}</span>
</div>
<div class="meta-item">
<span class="label">{{ t('due_date_label') }}</span>
<span class="value">{{ payment_terms.date }}</span>
</div>
</div>
<div class="customer-section">
<h4>{{ t('bill_to') }}</h4>
<p class="customer-name">{{ customer.name }}</p>
<p>{{ customer.address.street }} {{ customer.address.house_number }}</p>
<p>{{ customer.address.zip_code }} {{ customer.address.city }}</p>
<p>{{ customer.address.country }}</p>
{% if customer.vat_id %}
<p>{{ t('vat_id_label') }}: {{ customer.vat_id }}</p>
{% endif %}
</div>
</div>
What This Does
{{ company.logo_url }}: Displays the company logo (a pre-signed URL generated by the backend){{ t('invoice_title') }}: Translation key — outputs "Invoice" or "Rechnung" depending on customer language{{ invoice.invoice_number }},{{ invoice.invoice_date }}: Invoice metadata{{ payment_terms.date }}: The formatted due date{{ customer.address.* }}: Customer address sub-fields{% if customer.vat_id %}...{% endif %}: Only shows VAT ID if the customer has one
Step 2: Build the Invoice Content
The content section is the body of the invoice. Click the Invoice Content tab.
Content HTML
<table class="invoice-items">
<thead>
<tr>
<th class="col-description">{{ t('col_description') }}</th>
<th class="col-quantity">{{ t('col_quantity') }}</th>
<th class="col-unit">{{ t('col_unit') }}</th>
<th class="col-price">{{ t('col_unit_price') }}</th>
<th class="col-amount">{{ t('col_total') }}</th>
</tr>
</thead>
<tbody>
{% for position in positions %}
<tr class="{% if loop.last %}last-row{% endif %}">
<td class="col-description">
<strong>{{ position.title }}</strong>
{% if position.description %}
<div class="item-description">{{ position.description }}</div>
{% endif %}
</td>
<td class="col-quantity text-right">{{ position.quantity }}</td>
<td class="col-unit text-right">{{ position.unit }}</td>
<td class="col-price text-right">{{ position.unit_price }}</td>
<td class="col-amount text-right bold">{{ position.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="invoice-totals">
<div class="totals-row">
<span class="label">{{ t('subtotal') }}:</span>
<span class="amount">{{ invoice.subtotal }}</span>
</div>
<div class="totals-row">
<span class="label">{{ t('vat_label') }}:</span>
<span class="amount">{{ invoice.vat_total }}</span>
</div>
<div class="totals-row total-row">
<span class="label">{{ t('total') }}:</span>
<span class="amount">{{ invoice.total }}</span>
</div>
</div>
{% if IS_REVERSE_CHARGE %}
<div class="reverse-charge-notice">
<p>{{ t('reverse_charge_notice') }}</p>
</div>
{% endif %}
What This Does
{% for position in positions %}...{% endfor %}: Loops through each line item{{ position.title }},{{ position.quantity }}, etc.: Position data fieldsloop.last: Jinja2 loop variable — true on the last iteration, used for styling the final row{% if position.description %}: Only shows the description sub-line if it exists{{ t('col_description') }}: Translated column header{% if IS_REVERSE_CHARGE %}: Shows a reverse charge notice when automatically detected
Step 3: Build the Footer
The footer appears at the bottom of every page. Click the Footer tab.
Footer HTML
<div class="invoice-footer">
{% if bank_account.iban %}
<div class="footer-section">
<h4>{{ t('payment_info') }}</h4>
<p>{{ bank_account.bank_name }}</p>
<p>IBAN: {{ bank_account.iban }}</p>
{% if bank_account.bic %}
<p>BIC: {{ bank_account.bic }}</p>
{% endif %}
</div>
{% endif %}
{% if payment_terms.text %}
<div class="footer-section">
<h4>{{ t('payment_terms_label') }}</h4>
<p>{{ payment_terms.text }}</p>
</div>
{% endif %}
<div class="footer-legal">
<p>{{ company.name }} | {{ company.address }}</p>
{% if company.tax_id %}
<p>{{ t('tax_id_label') }}: {{ company.tax_id }}</p>
{% endif %}
</div>
</div>
What This Does
{% if bank_account.iban %}: Only shows bank details if IBAN is available{{ payment_terms.text }}: The fully formatted payment terms text (with days and date placeholders already resolved){{ company.tax_id }}: Company tax ID, shown conditionally
Step 4: Add CSS Styling
Switch to the CSS tab to style all sections.
Complete CSS
/* === GLOBAL STYLES === */
body {
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 11pt;
line-height: 1.6;
color: #333;
margin: 0;
padding: 20px;
}
/* === HEADER === */
.header-container {
max-width: 800px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #003366;
}
.company-logo {
max-width: 80px;
height: auto;
}
.company-name {
font-size: 24pt;
font-weight: bold;
color: #003366;
margin: 0;
}
.invoice-title {
font-size: 28pt;
font-weight: bold;
color: #333;
text-align: center;
margin: 30px 0;
}
.invoice-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 30px;
background-color: #f5f5f5;
padding: 15px;
border-radius: 4px;
}
.meta-item .label {
font-weight: bold;
color: #666;
}
.meta-item .value {
font-weight: bold;
color: #003366;
}
.customer-section {
margin: 30px 0;
}
.customer-section h4 {
font-size: 11pt;
font-weight: bold;
margin: 0 0 10px 0;
}
.customer-name {
font-weight: bold;
}
/* === LINE ITEMS TABLE === */
.invoice-items {
width: 100%;
border-collapse: collapse;
margin: 30px 0;
}
.invoice-items thead {
background-color: #003366;
color: white;
}
.invoice-items th {
padding: 12px;
text-align: left;
font-weight: bold;
}
.invoice-items th.col-quantity,
.invoice-items th.col-unit,
.invoice-items th.col-price,
.invoice-items th.col-amount {
text-align: right;
}
.invoice-items td {
padding: 12px;
vertical-align: top;
border-bottom: 1px solid #ddd;
}
.invoice-items td.col-quantity,
.invoice-items td.col-unit,
.invoice-items td.col-price,
.invoice-items td.col-amount {
text-align: right;
}
.invoice-items tr:nth-child(even) {
background-color: #f9f9f9;
}
.invoice-items tr.last-row {
border-bottom: 2px solid #333;
}
.item-description {
font-size: 10pt;
color: #666;
margin-top: 5px;
font-style: italic;
}
.text-right { text-align: right; }
.bold { font-weight: bold; }
/* === TOTALS === */
.invoice-totals {
width: 50%;
margin-left: auto;
margin-top: 20px;
}
.totals-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.totals-row .amount {
font-weight: 600;
min-width: 100px;
text-align: right;
}
.totals-row.total-row {
font-size: 14pt;
font-weight: bold;
color: #003366;
border-top: 2px solid #003366;
border-bottom: 2px solid #003366;
padding: 12px 0;
}
/* === FOOTER === */
.invoice-footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #ddd;
font-size: 10pt;
color: #666;
}
.footer-section {
margin-bottom: 15px;
}
.footer-section h4 {
font-size: 10pt;
font-weight: bold;
margin: 0 0 8px 0;
color: #333;
}
.footer-legal {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #ddd;
font-size: 9pt;
color: #999;
text-align: center;
}
.reverse-charge-notice {
margin-top: 20px;
padding: 10px;
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
font-size: 10pt;
}
Step 5: Add Translations
Click the Translations tab and enter the following JSON:
{
"en": {
"invoice_title": "Invoice",
"invoice_number_label": "Invoice #:",
"date_label": "Date:",
"due_date_label": "Due:",
"bill_to": "Bill To",
"vat_id_label": "VAT ID",
"col_description": "Description",
"col_quantity": "Qty",
"col_unit": "Unit",
"col_unit_price": "Unit Price",
"col_total": "Total",
"subtotal": "Subtotal",
"vat_label": "VAT",
"total": "Total Amount",
"reverse_charge_notice": "Reverse charge: VAT to be accounted for by the recipient.",
"payment_info": "Payment Information",
"payment_terms_label": "Payment Terms",
"tax_id_label": "Tax ID"
},
"de": {
"invoice_title": "Rechnung",
"invoice_number_label": "Rechnungsnummer:",
"date_label": "Datum:",
"due_date_label": "Fällig:",
"bill_to": "Rechnungsempfänger",
"vat_id_label": "USt-IdNr.",
"col_description": "Beschreibung",
"col_quantity": "Menge",
"col_unit": "Einheit",
"col_unit_price": "Einzelpreis",
"col_total": "Summe",
"subtotal": "Zwischensumme",
"vat_label": "USt.",
"total": "Gesamtsumme",
"reverse_charge_notice": "Steuerschuldnerschaft des Leistungsempfängers.",
"payment_info": "Zahlungsinformationen",
"payment_terms_label": "Zahlungsbedingungen",
"tax_id_label": "Steuernummer"
}
}
Every key you reference with {{ t('key_name') }} in your HTML must exist in the translations JSON for every language you support. Missing keys result in empty output.
Step 6: Test Your Template
Save and Preview
- Click Save in the Template IDE
- The version number increments automatically
- Check the PDF preview in the right panel — it uses the English translations and example data from the variable definitions
Generate a Test Invoice
- Go to the Invoices section
- Create a new invoice with sample data
- Select your new template
- Generate the PDF and review it
Verification Checklist
- Logo displays correctly
- Company name and address appear in the header
- Invoice number, date, and due date are shown
- Customer information is correct and complete
- All line items appear with proper formatting
- Subtotal, VAT, and total are displayed
- Payment information and terms appear in the footer
- No translation keys are missing (no empty labels)
- Colors and fonts look professional
- Tables do not break awkwardly across pages
Test Multiple Languages
- Create a test customer with language set to German
- Create an invoice for that customer using your template
- Verify all labels appear in German ("Rechnung", "Beschreibung", "Gesamtsumme", etc.)
Print Test
- Download the generated PDF
- Print it on paper or to a print-to-PDF viewer
- Check alignment, colors, and font rendering
Customizing the Design
Change the Color Scheme
Replace the primary color #003366 throughout the CSS with your brand color:
.header-top { border-bottom: 2px solid #2c5aa0; }
.company-name { color: #2c5aa0; }
.invoice-items thead { background-color: #2c5aa0; }
.totals-row.total-row { color: #2c5aa0; border-color: #2c5aa0; }
Adjust Logo Size
.company-logo {
max-width: 120px; /* Increase or decrease as needed */
}
Change the Font
body {
font-family: 'Georgia', serif;
}
Alternate Row Colors
.invoice-items tr:nth-child(even) {
background-color: #e8f0f7; /* Light blue tint */
}
Best Practices
Design
- Keep it simple and readable
- Use whitespace effectively — do not overcrowd sections
- Limit your palette to 2-3 colors
- Make important information (totals, invoice number) visually prominent
Code Quality
- Use semantic HTML tags
- Keep consistent indentation
- Use descriptive CSS class names
- Organize CSS by section with comments
- Avoid deeply nested HTML structures
Testing
- Always test with realistic data
- Check in multiple PDF readers
- Test both English and German
- Print to verify output quality
- Test with various scenarios: with and without VAT, single item vs. many items, with and without reverse charge
Your template is now complete. For more Jinja2 techniques, see Customize Templates. For adding more languages, see Template Translations.