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 linedouble- Double linedotted- Dotted linedashed- Dashed linenone- 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 heightexact- 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
CantSpliton 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!