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." },
],
},
Rich paragraph with a hyperlink
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
| Technique | Purpose |
|---|---|
headerRows: 0 on appointment table | Prevents 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 table | Associates each monetary cell with both its column label (Monthly/Annual) and its component row label |
scope: "row" on component cells | Explicitly 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 |
borderColor | Sets the border line color, pulled from the shared TABLE_BORDER constant for consistency across every table |
headerBackground | Applies a background fill to header rows/columns so they're visually distinguishable from data cells |
\n inside paragraph text | Produces line breaks within a single paragraph block, suitable for addresses and stacked signature fields |
richParagraph with runs | Mixes plain text, hyperlinks, and inline bold/color styling within a single paragraph |
link: "mailto:..." + underline: true | Renders a tappable email link with standard underline affordance |
bold: true + color: BRAND on a run | Highlights the company name inline without needing a separate block |
tag: "Artifact" on footer | Excludes 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 —
heading2sections must sit under aheading1parent. tagged: trueandstructureRoot: "Document"are required at document creation.richParagraphruns are tagged inline within the parent paragraph structure; no additional tagging is needed.