Skip to main content

Cookbook: Joining Letter

Generate a formal employment joining letter with appointment details, a compensation breakdown, benefits, terms and conditions, and a dual-signature acceptance block using renderTemplate().

Quick start

import { createDocument, rgb } from "@barrierbreak/a11ydocs-pdf";

async function ttf(url: string): Promise<Uint8Array> {
return new Uint8Array(await (await fetch(url)).arrayBuffer());
}

const cdn = "https://cdn.jsdelivr.net/npm/@expo-google-fonts";
const FONTS = {
inter400: `${cdn}/inter/400Regular/Inter_400Regular.ttf`,
inter700: `${cdn}/inter/700Bold/Inter_700Bold.ttf`,
};

const companyLogo = fetch("examples/assets/company-logo.png");
const logo = await companyLogo.then((res) => res.arrayBuffer());

const doc = createDocument({
language: "en-US",
conformance: "pdfua-1",
tagged: true,
structureRoot: "Document",
title: "Joining Letter - John Doe",
info: { author: "Lorem Ipsum" },
defaults: { fontSize: 11, color: rgb(0.1, 0.1, 0.15) },
});

const [inter400, inter700] = await Promise.all([
ttf(FONTS.inter400),
ttf(FONTS.inter700),
]);

doc.registerFontFamily("Inter", { regular: inter400, bold: inter700 });

Color palette

const BRAND = rgb(0.05, 0.25, 0.55);
const MUTED = rgb(0.45, 0.45, 0.5);
const TABLE_BORDER = rgb(0.72, 0.76, 0.82);
const TABLE_HEADER = rgb(0.92, 0.94, 0.98);

Building the letter

Letter header and salutation

After the logo, an h1 block renders the company name as the document title. Plain paragraph blocks handle the recipient address, salutation, subject line, and opening body text:

{
type: "png",
data: logo,
options: { altText: "Lorem Ipsum Logo", maxWidth: 100 },
},
{ type: "heading", text: "Lorem Ipsum", options: { level: 1 } },

{
type: "paragraph",
text: "To,\nMr. John Doe\n47, Lorem ipsum dolor sit amet",
options: { marginBottom: 20 },
},
{ type: "paragraph", text: "Dear Mr. John Doe," },
{ type: "paragraph", text: "Subject: Offer of Employment - Senior Software Engineer" },
{
type: "paragraph",
text: "We are delighted to welcome you to Lorem Ipsum...",
},

Newlines (\n) inside a paragraph text string produce line breaks within the same paragraph block, which is the correct pattern for multi-line addresses.

Appointment details table

The appointment table uses headerRows: 0 because none of its rows are column headers — every row is a label/value pair. The label cells carry header: true and bold: true to mark them as row headers semantically without triggering column-header treatment:

{
type: "table",
rows: [
[{ text: "Full Name", header: true, bold: true }, { text: "Mr. John Doe" }],
[{ text: "Employee ID", header: true, bold: true }, { text: "SS-EMP-2026-0148" }],
[{ text: "Designation", header: true, bold: true }, { text: "Senior Software Engineer" }],
[{ text: "Department", header: true, bold: true }, { text: "Engineering - Platform Team" }],
[{ text: "Date of Joining", header: true, bold: true }, { text: "01 July 2026" }],
[{ text: "Employment Type", header: true, bold: true }, { text: "Full-Time, Permanent" }],
[{ text: "Work Location", header: true, bold: true }, { text: "47, Lorem ipsum dolor sit amet" }],
[{ text: "Work Mode", header: true, bold: true }, { text: "Hybrid (3 days in-office, 2 days remote)" }],
[{ text: "Probation Period", header: true, bold: true }, { text: "6 months (01 Jul 2026 - 31 Dec 2026)" }],
],
options: {
headerRows: 0,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

Compensation table

The compensation table uses both headerRows: 1 (column headers: Component, Monthly, Annual) and headerColumns: 1 with scope: "row" on each component name, so screen readers can associate every monetary cell with both its column label and its row label:

{
type: "table",
rows: [
[
{ text: "Component", header: true, bold: true },
{ text: "Monthly (Rs.)", header: true, bold: true },
{ text: "Annual (Rs.)", header: true, bold: true },
],
[{ text: "Basic Pay", header: true, scope: "row" }, { text: "Rs. 75,000.00" }, { text: "Rs. 9,00,000.00" }],
[{ text: "House Rent Allowance", header: true, scope: "row" }, { text: "Rs. 30,000.00" }, { text: "Rs. 3,60,000.00" }],
[{ text: "Special Allowance", header: true, scope: "row" }, { text: "Rs. 20,000.00" }, { text: "Rs. 2,40,000.00" }],
[{ text: "Transport Allowance", header: true, scope: "row" }, { text: "Rs. 3,200.00" }, { text: "Rs. 38,400.00" }],
[{ text: "Medical Allowance", header: true, scope: "row" }, { text: "Rs. 1,250.00" }, { text: "Rs. 15,000.00" }],
[{ text: "Employer PF Contribution",header: true, scope: "row" }, { text: "Rs. 9,000.00" }, { text: "Rs. 1,08,000.00" }],
[{ text: "Gross CTC", header: true, scope: "row" }, { text: "Rs. 1,38,450.00" }, { text: "Rs. 16,61,400.00" }],
],
options: {
headerRows: 1,
headerColumns: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

List blocks

list blocks handle all bulleted content — additional benefits, terms and conditions, and the joining document checklist. Each item takes a body string:

{
type: "list",
items: [
{ body: "Group Term Life Insurance: Coverage of Rs. 50,00,000." },
{ body: "Annual Leave: 18 days of paid annual leave per calendar year." },
{ body: "Sick Leave: 12 days per calendar year." },
],
},

Use richParagraph with a runs array when part of a paragraph needs different styling — such as an inline hyperlink or bold brand name:

// Inline email link
{
type: "richParagraph",
runs: [
{ text: "For any queries regarding joining, please contact our HR team at " },
{ text: "hr@loremipsum.in", link: "mailto:hr@loremipsum.in", underline: true },
],
},

// Inline bold brand name with brand color
{
type: "richParagraph",
runs: [
{ text: "We look forward to having you on board and wish you a rewarding career at " },
{ text: "Lorem Ipsum", bold: true, color: BRAND },
{ text: ". Welcome to the team!" },
],
},

Each run inherits the page font and base font size; only the properties you specify on the run are overridden.

Dual-signature acceptance table

The acceptance block uses a two-column table where both columns are marked as headers (headerRows: 1) and the signature lines are plain text strings with \n separating each field:

{
type: "table",
rows: [
[
{ text: "For Lorem Ipsum.", header: true, bold: true },
{ text: "Accepted by Employee", header: true, bold: true },
],
[
"Signature: ___________________\nName: Ms. Jane Doe\nDesignation: Chief HR Officer\nDate: 16 June 2026",
"Signature: ___________________\nName: Mr. John Doe\nDate: ___________________",
],
],
options: {
headerRows: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

The blank underscores give signatories space to write; the \n characters produce the stacked field layout within a single cell.

Full template

doc.renderTemplate({
autoOutline: { maxLevel: 6 },

page: {
size: "A4",
margin: 56,
font: "Inter",
spacing: {
paragraphAfter: 8,
headingBefore: { 2: 18, 3: 12 },
headingAfter: { 1: 4, 2: 8, 3: 6 },
tableBefore: 6,
tableAfter: 14,
},
},

styles: {
heading1: { fontSize: 22, color: BRAND, marginTop: 20 },
heading2: { fontSize: 16, color: BRAND },
paragraph: { fontSize: 11, lineHeight: 17 },
muted: { fontSize: 9, color: MUTED, lineHeight: 14 },
},

blocks: [
// ── Company header ───────────────────────────────────
{
type: "png",
data: logo,
options: { altText: "Lorem Ipsum Logo", maxWidth: 100 },
},
{ type: "heading", text: "Lorem Ipsum", options: { level: 1 } },

// ── Salutation and opening ───────────────────────────
{
type: "paragraph",
text: "To,\nMr. John Doe\n47, Lorem ipsum dolor sit amet",
options: { marginBottom: 20 },
},
{ type: "paragraph", text: "Dear Mr. John Doe," },
{
type: "paragraph",
text: "Subject: Offer of Employment - Senior Software Engineer",
},
{
type: "paragraph",
text: "We are delighted to welcome you to Lorem Ipsum. Following the successful completion of our selection process, we are pleased to offer you the position of Senior Software Engineer within our Engineering department. This letter confirms the terms and conditions of your employment with us.",
},

// ── Appointment Details ──────────────────────────────
{ type: "heading", text: "Appointment Details", options: { level: 2 } },
{
type: "table",
rows: [
[
{ text: "Full Name", header: true, bold: true },
{ text: "Mr. John Doe" },
],
[
{ text: "Employee ID", header: true, bold: true },
{ text: "SS-EMP-2026-0148" },
],
[
{ text: "Designation", header: true, bold: true },
{ text: "Senior Software Engineer" },
],
[
{ text: "Department", header: true, bold: true },
{ text: "Engineering - Platform Team" },
],
[
{ text: "Date of Joining", header: true, bold: true },
{ text: "01 July 2026" },
],
[
{ text: "Employment Type", header: true, bold: true },
{ text: "Full-Time, Permanent" },
],
[
{ text: "Work Location", header: true, bold: true },
{ text: "47, Lorem ipsum dolor sit amet" },
],
[
{ text: "Work Mode", header: true, bold: true },
{ text: "Hybrid (3 days in-office, 2 days remote)" },
],
[
{ text: "Probation Period", header: true, bold: true },
{ text: "6 months (01 Jul 2026 - 31 Dec 2026)" },
],
],
options: {
headerRows: 0,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

// ── Compensation & Benefits ──────────────────────────
{ type: "heading", text: "Compensation & Benefits", options: { level: 2 } },
{
type: "table",
rows: [
[
{ text: "Component", header: true, bold: true },
{ text: "Monthly (Rs.)", header: true, bold: true },
{ text: "Annual (Rs.)", header: true, bold: true },
],
[
{ text: "Basic Pay", header: true, scope: "row" },
{ text: "Rs. 75,000.00" },
{ text: "Rs. 9,00,000.00" },
],
[
{ text: "House Rent Allowance", header: true, scope: "row" },
{ text: "Rs. 30,000.00" },
{ text: "Rs. 3,60,000.00" },
],
[
{ text: "Special Allowance", header: true, scope: "row" },
{ text: "Rs. 20,000.00" },
{ text: "Rs. 2,40,000.00" },
],
[
{ text: "Transport Allowance", header: true, scope: "row" },
{ text: "Rs. 3,200.00" },
{ text: "Rs. 38,400.00" },
],
[
{ text: "Medical Allowance", header: true, scope: "row" },
{ text: "Rs. 1,250.00" },
{ text: "Rs. 15,000.00" },
],
[
{ text: "Employer PF Contribution", header: true, scope: "row" },
{ text: "Rs. 9,000.00" },
{ text: "Rs. 1,08,000.00" },
],
[
{ text: "Gross CTC", header: true, scope: "row" },
{ text: "Rs. 1,38,450.00" },
{ text: "Rs. 16,61,400.00" },
],
],
options: {
headerRows: 1,
headerColumns: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

// ── Additional Benefits ──────────────────────────────
{ type: "heading", text: "Additional Benefits", options: { level: 2 } },
{
type: "list",
items: [
{ body: "Group Term Life Insurance: Coverage of Rs. 50,00,000." },
{
body: "Annual Leave: 18 days of paid annual leave per calendar year.",
},
{ body: "Sick Leave: 12 days per calendar year." },
],
},

// ── Terms & Conditions ───────────────────────────────
{ type: "heading", text: "Terms & Conditions", options: { level: 2 } },
{
type: "list",
items: [
{
body: "This offer is contingent upon the satisfactory verification of your educational qualifications, previous employment records, and background check.",
},
{
body: "You are required to submit all original documents for verification on or before your date of joining.",
},
{
body: "During the probation period, either party may terminate employment with 15 days written notice. Post-confirmation, the notice period is 60 days.",
},
],
},

// ── Joining Instructions ─────────────────────────────
{ type: "heading", text: "Joining Instructions", options: { level: 2 } },
{
type: "paragraph",
text: "Please report to the HR department at our office on 01 July 2026 at 9:30 AM. Kindly carry the following documents in original along with one set of self-attested photocopies:",
},
{
type: "list",
items: [
{ body: "Government-issued photo ID" },
{ body: "Last 3 months' salary slips and latest Form 16" },
{
body: "Experience letters and relieving letters from all previous employers",
},
{ body: "2 recent passport-size photographs" },
],
},
{
type: "richParagraph",
runs: [
{
text: "For any queries regarding joining, please contact our HR team at ",
},
{
text: "hr@loremipsum.in",
link: "mailto:hr@loremipsum.in",
underline: true,
},
],
},

// ── Acceptance ───────────────────────────────────────
{ type: "heading", text: "Acceptance", options: { level: 2 } },
{
type: "paragraph",
text: "Please indicate your acceptance of this offer by signing and returning a copy of this letter on or before 25 June 2026. By accepting this offer, you confirm that you are not bound by any non-compete or other restrictive agreements that would prevent you from joining Lorem Ipsum.",
},
{
type: "table",
rows: [
[
{ text: "For Lorem Ipsum.", header: true, bold: true },
{ text: "Accepted by Employee", header: true, bold: true },
],
[
"Signature: ___________________\nName: Ms. Jane Doe\nDesignation: Chief HR Officer\nDate: 16 June 2026",
"Signature: ___________________\nName: Mr. John Doe\nDate: ___________________",
],
],
options: {
headerRows: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER,
},
},

{
type: "richParagraph",
runs: [
{
text: "We look forward to having you on board and wish you a rewarding career at ",
},
{ text: "Lorem Ipsum", bold: true, color: BRAND },
{ text: ". Welcome to the team!" },
],
},
],

footer: ({ pageNumber, totalPages }) => [
{
type: "paragraph",
text: "Generated by Lorem Ipsum",
options: {
fontSize: 10,
tag: pageNumber === totalPages ? "P" : "Artifact",
},
},
],

pageNumber: { region: "footer", align: "right" },
});

export default doc;

Key techniques

TechniquePurpose
headerRows: 0 on appointment tablePrevents any row from being treated as a column header; all label cells are row headers via header: true instead
headerRows: 1 + headerColumns: 1 on compensation tableAssociates each monetary cell with both its column label (Monthly/Annual) and its component row label
scope: "row" on component cellsExplicitly scopes each compensation label to its own row for screen reader association
borders: "all"Draws borders around every cell rather than just the table's outer edge
borderColorSets the border line color, pulled from the shared TABLE_BORDER constant for consistency across every table
headerBackgroundApplies a background fill to header rows/columns so they're visually distinguishable from data cells
\n inside paragraph textProduces line breaks within a single paragraph block, suitable for addresses and stacked signature fields
richParagraph with runsMixes plain text, hyperlinks, and inline bold/color styling within a single paragraph
link: "mailto:..." + underline: trueRenders a tappable email link with standard underline affordance
bold: true + color: BRAND on a runHighlights the company name inline without needing a separate block
tag: "Artifact" on footerExcludes the repeated footer line from the logical reading order on non-final pages

PDF/UA conformance

This template targets conformance: "pdfua-1". To keep the document valid:

  • Every image block must include a non-empty altText.
  • Decorative repeated elements such as intermediate-page footers should use tag: "Artifact".
  • Heading levels must not skip — heading2 sections must sit under a heading1 parent.
  • tagged: true and structureRoot: "Document" are required at document creation.
  • richParagraph runs are tagged inline within the parent paragraph structure; no additional tagging is needed.