Skip to main content

Tables in OOXML

Table Structure Overview​

Tables in OOXML are more complex than HTML tables, with precise control over layout, borders, and cell properties.


Complete Table Structure​

<w:tbl>
<!-- 1. Table Properties (optional but recommended) -->
<w:tblPr>
<w:tblStyle w:val="TableGrid"/>
<w:tblW w:w="5000" w:type="pct"/>
<w:tblBorders>...</w:tblBorders>
<w:tblCellMar>...</w:tblCellMar>
</w:tblPr>

<!-- 2. Table Grid (REQUIRED) -->
<w:tblGrid>
<w:gridCol w:w="2500"/>
<w:gridCol w:w="2500"/>
</w:tblGrid>

<!-- 3. Rows (at least one required) -->
<w:tr>
<w:trPr>...</w:trPr>
<w:tc>
<w:tcPr>...</w:tcPr>
<w:p>...</w:p> <!-- REQUIRED -->
</w:tc>
<w:tc>...</w:tc>
</w:tr>
</w:tbl>

Table Properties (w:tblPr)​

Table Width​

<w:tblW w:w="5000" w:type="pct"/>  <!-- 100% of page width -->

Width types:

  • pct - Percentage (5000 = 100%)
  • dxa - Twentieths of a point (absolute)
  • auto - Automatic sizing

Examples:

<!-- 100% width -->
<w:tblW w:w="5000" w:type="pct"/>

<!-- 6 inches (8640 twips) -->
<w:tblW w:w="8640" w:type="dxa"/>

<!-- Auto width -->
<w:tblW w:w="0" w:type="auto"/>

Table Borders​

<w:tblBorders>
<w:top w:val="single" w:sz="4" w:color="000000"/>
<w:left w:val="single" w:sz="4" w:color="000000"/>
<w:bottom w:val="single" w:sz="4" w:color="000000"/>
<w:right w:val="single" w:sz="4" w:color="000000"/>
<w:insideH w:val="single" w:sz="4" w:color="000000"/>
<w:insideV w:val="single" w:sz="4" w:color="000000"/>
</w:tblBorders>

Border styles (w:val):

  • single - Single line
  • double - Double line
  • dotted - Dotted line
  • dashed - Dashed line
  • none - No border

Border size (w:sz): Eighths of a point (4 = 0.5pt)

Border color (w:color): Hex color code

Table Cell Margins​

<w:tblCellMar>
<w:top w:w="0" w:type="dxa"/>
<w:left w:w="108" w:type="dxa"/> <!-- Default margin -->
<w:bottom w:w="0" w:type="dxa"/>
<w:right w:w="108" w:type="dxa"/>
</w:tblCellMar>

AppraiSys pattern (from DefaultDocument.cs):

new TableCellMarginDefault(
new TopMargin { Width = "0", Type = TableWidthUnitValues.Dxa },
new TableCellLeftMargin { Width = 108, Type = TableWidthValues.Dxa },
new BottomMargin { Width = "0", Type = TableWidthUnitValues.Dxa },
new TableCellRightMargin { Width = 108, Type = TableWidthValues.Dxa }
)

Table Alignment​

<w:jc w:val="center"/>  <!-- Center table on page -->

Values: left, center, right

Table Layout​

<w:tblLayout w:type="fixed"/>

Types:

  • fixed - Fixed column widths (faster rendering)
  • autofit - Columns adjust to content

Table Grid (w:tblGrid)​

Purpose: Defines the column structure

Required: Yes, even for variable-width tables

<w:tblGrid>
<w:gridCol w:w="2000"/> <!-- Column 1: 2000 twips -->
<w:gridCol w:w="3000"/> <!-- Column 2: 3000 twips -->
<w:gridCol w:w="2000"/> <!-- Column 3: 2000 twips -->
</w:tblGrid>

Why needed: Defines the underlying grid that cells span across


Table Rows (w:tr)​

Row Structure​

<w:tr>
<!-- Row properties (optional) -->
<w:trPr>
<w:trHeight w:val="500" w:hRule="atLeast"/>
<w:cantSplit/>
</w:trPr>

<!-- Cells (at least one required) -->
<w:tc>...</w:tc>
<w:tc>...</w:tc>
</w:tr>

Row Height​

<w:trHeight w:val="500" w:hRule="atLeast"/>

Rules (w:hRule):

  • auto - Automatic (default)
  • atLeast - Minimum height
  • exact - Fixed height

Value: Height in twips (20 twips = 1 point)

Prevent Row Breaking (w:cantSplit)​

<w:trPr>
<w:cantSplit/> <!-- Keep row together across pages -->
</w:trPr>

AppraiSys usage: All table rows in your system use this!

From DefaultDocument.cs:

style.Append(new TableRowProperties(
new CantSplit()
));

Row Headers (Repeat on Pages)​

<w:trPr>
<w:tblHeader/> <!-- Repeat as header on each page -->
</w:trPr>

Table Cells (w:tc)​

Cell Structure​

<w:tc>
<!-- Cell properties (optional) -->
<w:tcPr>
<w:tcW w:w="2500" w:type="dxa"/>
<w:vMerge w:val="restart"/>
<w:gridSpan w:val="2"/>
<w:tcBorders>...</w:tcBorders>
<w:shd w:fill="FFFF00"/>
</w:tcPr>

<!-- Content (at least one paragraph REQUIRED) -->
<w:p>
<w:r><w:t>Cell content</w:t></w:r>
</w:p>
</w:tc>

Cell Width​

<w:tcW w:w="2500" w:type="dxa"/>

Important: Cell widths should match grid columns or span multiple columns

Cell Borders​

<w:tcBorders>
<w:top w:val="single" w:sz="4"/>
<w:left w:val="single" w:sz="4"/>
<w:bottom w:val="single" w:sz="4"/>
<w:right w:val="single" w:sz="4"/>
</w:tcBorders>

Override table borders for individual cells

Cell Shading (Background)​

<w:shd w:fill="FFFF00" w:val="clear"/>  <!-- Yellow background -->

Vertical Alignment​

<w:vAlign w:val="center"/>

Values: top, center, bottom


Cell Merging​

Horizontal Merge (Spanning Columns)​

Use w:gridSpan to span multiple columns:

<w:tblGrid>
<w:gridCol w:w="2000"/>
<w:gridCol w:w="2000"/>
<w:gridCol w:w="2000"/>
</w:tblGrid>

<w:tr>
<!-- Cell spans columns 1 and 2 -->
<w:tc>
<w:tcPr>
<w:tcW w:w="4000" w:type="dxa"/>
<w:gridSpan w:val="2"/> <!-- Spans 2 columns -->
</w:tcPr>
<w:p><w:r><w:t>Merged cell</w:t></w:r></w:p>
</w:tc>

<!-- Cell in column 3 -->
<w:tc>
<w:tcPr><w:tcW w:w="2000" w:type="dxa"/></w:tcPr>
<w:p><w:r><w:t>Normal cell</w:t></w:r></w:p>
</w:tc>
</w:tr>

Vertical Merge (Spanning Rows)​

Use w:vMerge with restart and continue:

<!-- Row 1: Start merge -->
<w:tr>
<w:tc>
<w:tcPr>
<w:vMerge w:val="restart"/> <!-- Start vertical merge -->
</w:tcPr>
<w:p><w:r><w:t>Merged cell (shows content)</w:t></w:r></w:p>
</w:tc>
<w:tc>
<w:p><w:r><w:t>Normal cell</w:t></w:r></w:p>
</w:tc>
</w:tr>

<!-- Row 2: Continue merge -->
<w:tr>
<w:tc>
<w:tcPr>
<w:vMerge w:val="continue"/> <!-- Continue merge -->
</w:tcPr>
<w:p/> <!-- Empty paragraph required -->
</w:tc>
<w:tc>
<w:p><w:r><w:t>Normal cell</w:t></w:r></w:p>
</w:tc>
</w:tr>

<!-- Row 3: Continue merge -->
<w:tr>
<w:tc>
<w:tcPr>
<w:vMerge w:val="continue"/>
</w:tcPr>
<w:p/>
</w:tc>
<w:tc>
<w:p><w:r><w:t>Normal cell</w:t></w:r></w:p>
</w:tc>
</w:tr>

Key points:

  • First cell: w:vMerge="restart" with content
  • Subsequent cells: w:vMerge="continue" with empty paragraph
  • All merged cells must have same column position

⚠️ Common mistake: Forgetting empty paragraph in continued cells


AppraiSys Table Patterns​

NormalTable Style​

From DefaultDocument.cs:

CreateTableStyle("NormalTable", "Normal Table", 99)

Properties:

  • Zero table indentation
  • 108 twips left/right cell margins
  • CantSplit on rows
  • No borders (set per table)

PropertyImagesTable Style​

From DefaultDocument.cs:

CreatePropertyImagesTableStyle()

Properties:

  • Center aligned
  • 100 twips cell spacing
  • No borders
  • Zero cell margins
  • Top vertical alignment
  • Used for property photo galleries

Creating Tables in Code​

// From your codebase pattern
var table = new Table();

// Table properties
var tableProperties = new TableProperties(
new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct },
new TableBorders(
new TopBorder { Val = BorderValues.Single, Size = 4 },
new BottomBorder { Val = BorderValues.Single, Size = 4 },
new LeftBorder { Val = BorderValues.Single, Size = 4 },
new RightBorder { Val = BorderValues.Single, Size = 4 },
new InsideHorizontalBorder { Val = BorderValues.Single, Size = 4 },
new InsideVerticalBorder { Val = BorderValues.Single, Size = 4 }
),
new TableRowProperties(new CantSplit()),
new TableLayout { Type = TableLayoutValues.Fixed },
new TableJustification { Val = TableRowAlignmentValues.Center }
);
table.AppendChild(tableProperties);

// Grid
var tableGrid = new TableGrid(
new GridColumn { Width = "2500" },
new GridColumn { Width = "2500" }
);
table.AppendChild(tableGrid);

// Rows
var row = CreateTableRow("Header 1", "Header 2", bold: true);
table.AppendChild(row);

Executive Summary Table​

From SubjectPropertyInformationSubstitutor.cs:

var table = new Table();
var tableProperties = new TableProperties(
new TableWidth { Width = "4500", Type = TableWidthUnitValues.Pct },
new TableBorders(
new TopBorder { Val = BorderValues.Lightning2 },
new LeftBorder { Val = BorderValues.Lightning2 },
new BottomBorder { Val = BorderValues.Lightning2 },
new RightBorder { Val = BorderValues.Lightning2 },
new InsideHorizontalBorder { Val = BorderValues.Lightning2 },
new InsideVerticalBorder { Val = BorderValues.None }
),
new TableRowProperties(new CantSplit()),
new TableLayout { Type = TableLayoutValues.Fixed },
new TableJustification { Val = TableRowAlignmentValues.Center }
);

Pattern: 90% width, lightning borders, no inside vertical borders


Nested Tables​

Tables can be nested inside table cells:

<w:tc>
<w:p>
<w:r><w:t>Header text</w:t></w:r>
</w:p>

<!-- Nested table -->
<w:tbl>
<w:tblPr>...</w:tblPr>
<w:tblGrid>...</w:tblGrid>
<w:tr>...</w:tr>
</w:tbl>
</w:tc>

Requirements:

  • Cell must have at least one paragraph before table
  • Nested table is fully independent
  • Has own grid, borders, properties

Use case: Complex layouts, forms within tables


Common Table Mistakes​

❌ Mistake 1: Missing Table Grid​

<!-- WRONG: No tblGrid -->
<w:tbl>
<w:tblPr>...</w:tblPr>
<w:tr>...</w:tr>
</w:tbl>

Fix: Always include w:tblGrid

<!-- CORRECT -->
<w:tbl>
<w:tblPr>...</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="5000"/>
</w:tblGrid>
<w:tr>...</w:tr>
</w:tbl>

❌ Mistake 2: Cell Without Paragraph​

<!-- WRONG -->
<w:tc>
<w:tcPr>...</w:tcPr>
<w:r><w:t>Text</w:t></w:r>
</w:tc>

Fix: Always have at least one paragraph

<!-- CORRECT -->
<w:tc>
<w:tcPr>...</w:tcPr>
<w:p>
<w:r><w:t>Text</w:t></w:r>
</w:p>
</w:tc>

❌ Mistake 3: Table Inside Run​

<!-- WRONG -->
<w:p>
<w:r>
<w:tbl>...</w:tbl>
</w:r>
</w:p>

Fix: Tables are block-level

<!-- CORRECT -->
<w:body>
<w:p>...</w:p>
<w:tbl>...</w:tbl>
</w:body>

❌ Mistake 4: Grid Mismatch​

<!-- WRONG: Grid has 2 columns, but cell spans 3 -->
<w:tblGrid>
<w:gridCol w:w="2000"/>
<w:gridCol w:w="2000"/>
</w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr>
<w:gridSpan w:val="3"/> <!-- ERROR! -->
</w:tcPr>
<w:p>...</w:p>
</w:tc>
</w:tr>

Fix: Grid must have enough columns


Complete Table Example​

<w:tbl>
<!-- Properties -->
<w:tblPr>
<w:tblStyle w:val="NormalTable"/>
<w:tblW w:w="5000" w:type="pct"/>
<w:tblBorders>
<w:top w:val="single" w:sz="4" w:color="000000"/>
<w:left w:val="single" w:sz="4" w:color="000000"/>
<w:bottom w:val="single" w:sz="4" w:color="000000"/>
<w:right w:val="single" w:sz="4" w:color="000000"/>
<w:insideH w:val="single" w:sz="4" w:color="000000"/>
<w:insideV w:val="single" w:sz="4" w:color="000000"/>
</w:tblBorders>
<w:tblLayout w:type="fixed"/>
<w:jc w:val="center"/>
</w:tblPr>

<!-- Grid: 2 equal columns -->
<w:tblGrid>
<w:gridCol w:w="3000"/>
<w:gridCol w:w="3000"/>
</w:tblGrid>

<!-- Header row -->
<w:tr>
<w:trPr><w:cantSplit/></w:trPr>
<w:tc>
<w:tcPr>
<w:tcW w:w="3000" w:type="dxa"/>
<w:shd w:fill="CCCCCC"/>
</w:tcPr>
<w:p>
<w:pPr><w:jc w:val="center"/></w:pPr>
<w:r>
<w:rPr><w:b/></w:rPr>
<w:t>Field</w:t>
</w:r>
</w:p>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3000" w:type="dxa"/>
<w:shd w:fill="CCCCCC"/>
</w:tcPr>
<w:p>
<w:pPr><w:jc w:val="center"/></w:pPr>
<w:r>
<w:rPr><w:b/></w:rPr>
<w:t>Value</w:t>
</w:r>
</w:p>
</w:tc>
</w:tr>

<!-- Data row -->
<w:tr>
<w:trPr><w:cantSplit/></w:trPr>
<w:tc>
<w:tcPr><w:tcW w:w="3000" w:type="dxa"/></w:tcPr>
<w:p>
<w:r><w:t>Property Type</w:t></w:r>
</w:p>
</w:tc>
<w:tc>
<w:tcPr><w:tcW w:w="3000" w:type="dxa"/></w:tcPr>
<w:p>
<w:r><w:t>Single Family</w:t></w:r>
</w:p>
</w:tc>
</w:tr>
</w:tbl>

Helper Methods​

Create Table Cell​

private TableCell CreateCell(string text, int width, bool bold = false)
{
var cell = new TableCell();

cell.Append(new TableCellProperties(
new TableCellWidth { Width = width.ToString(), Type = TableWidthUnitValues.Dxa }
));

var paragraph = new Paragraph();
var run = new Run();

if (bold)
{
run.Append(new RunProperties(new Bold()));
}
run.Append(new Text(text));

paragraph.Append(run);
cell.Append(paragraph);

return cell;
}

Create Table Row​

private TableRow CreateRow(params TableCell[] cells)
{
var row = new TableRow();
row.Append(new TableRowProperties(new CantSplit()));

foreach (var cell in cells)
{
row.Append(cell);
}

return row;
}

Quick Reference​

Minimum Valid Table​

<w:tbl>
<w:tblGrid>
<w:gridCol w:w="5000"/>
</w:tblGrid>
<w:tr>
<w:tc>
<w:p/>
</w:tc>
</w:tr>
</w:tbl>

Element Order​

w:tbl
├─ w:tblPr (optional, but first if present)
├─ w:tblGrid (required)
└─ w:tr (one or more)
├─ w:trPr (optional, but first if present)
└─ w:tc (one or more)
├─ w:tcPr (optional, but first if present)
└─ w:p (required, at least one)

Next Steps​

➡️ Next: Numbering and Lists

Related:


💡 Remember: Every table cell must contain at least one paragraph—this is the #1 table mistake!