✈️ FlightPlan Developer Guide

← Back to Guide

Table Elements

FlightPlan offers two ways to build tables. Use Table for hand-built tables where you control every cell. Use SimpleTable when you have a DataTable from the database and want it rendered with optional column formatters.

Choosing a Table Style

Neither Table nor SimpleTable applies any styling by default. You always pass a CSS class to control the look. There are three common choices, in order of effort:

Choice 1: No class — unstyled (probably not what you want)

If you construct a table with no class, the browser renders a bare HTML table — no borders, no padding, no header background. Cells are just text with whitespace separators. Useful for very rare cases where you'll style it externally; almost never the right choice.

// No class — produces an invisible table
content.Add(new SimpleTable(data)
    .DefineColumn("employee_name", "Name")
    .DefineColumn("role",          "Role"));
Renders as:
Name Role
Ada Lovelace Engineer
Grace Hopper Architect

Choice 2: CoreCss.BorderTable — minimal Excel-style grid

The framework's built-in default for clean, functional tables. Defined in CoreStyles, so it works in any app without adding stylesheet code. Gray header, thin borders, hover highlight on rows. Use this when you just want a readable data grid and don't care about visual flourish.

using CoreCss = FlightPlan.Core.Dom.Styles.CoreStyles.Css;

content.Add(new SimpleTable(data, CoreCss.BorderTable)
    .DefineColumn("employee_name", "Name")
    .DefineColumn("role",          "Role"));
Renders as:
Name Role
Ada Lovelace Engineer
Grace Hopper Architect

Choice 3: Your own suite or app CSS class — full control

When you want a particular look (colored headers, alternating rows, condensed padding), define a class in your app's StyleLibrary and pass its name. StateLawTable uses "data-table"; SCM uses its own. Whatever you define, just pass the string.

// In YourAppStyles.cs:
lib.DefineSelector(".my-table th")
    .Property("background", "#2c3e50")
    .Property("color", "white")
    .Property("padding", "12px");

lib.DefineSelector(".my-table td")
    .Property("padding", "10px")
    .Property("border-bottom", "1px solid #eee");

// In a page:
content.Add(new SimpleTable(data, "my-table")
    .DefineColumn("employee_name", "Name"));

Quick Tables: Table.AddHeaderRow / AddRow

The fastest path for hand-built tables. AddHeaderRow takes column titles; AddRow takes cell values as objects (auto-converted to strings). Internally these create the Thead, Tbody, Tr, Th, and Td for you. Pass CoreCss.BorderTable (or your own class) to get visible styling.

var table = content.Add(new Table(CoreCss.BorderTable));
table.AddHeaderRow("Name", "Role", "Hire Date");
table.AddRow("Ada Lovelace", "Engineer", "1843-12-10");
table.AddRow("Grace Hopper", "Architect", "1944-06-01");
table.AddRow("Alan Turing",  "Researcher", "1936-05-28");
Renders as:
Name Role Hire Date
Ada Lovelace Engineer 1843-12-10
Grace Hopper Architect 1944-06-01
Alan Turing Researcher 1936-05-28

Manual Construction: Thead, Tbody, Tr, Th, Td

Use the explicit form when you need control — custom CSS on individual rows or cells, child elements inside cells, or anything beyond plain text.

var table = content.Add(new Table(CoreCss.BorderTable));

var thead = table.Add(new Thead());
var headRow = thead.Add(new Tr());
headRow.Add(new Th("Product"));
headRow.Add(new Th("Price"));

var tbody = table.Add(new Tbody());
var row1 = tbody.Add(new Tr());
row1.Add(new Td("Widget"));
row1.Add(new Td("$12.50"));

var row2 = tbody.Add(new Tr());
row2.Add(new Td("Gadget"));
row2.Add(new Td("$45.00"));
Renders as:
Product Price
Widget $12.50
Gadget $45.00

Cell Spanning: WithColSpan

Both Th and Td support WithColSpan(n) to span multiple columns. Useful for section headers inside the table body.

var table = content.Add(new Table(CoreCss.BorderTable));
table.AddHeaderRow("Item", "Q1", "Q2", "Q3", "Q4");

var sectionRow = table.AddEmptyRow();
sectionRow.Add(new Td("— 2025 Sales —").WithColSpan(5));

table.AddRow("Widget", "100", "120", "95", "140");
table.AddRow("Gadget", "80",  "90",  "110", "130");
Renders as:
Item Q1 Q2 Q3 Q4
— 2025 Sales —
Widget 100 120 95 140
Gadget 80 90 110 130

Cells with Child Elements

A Td with no constructor text but with child elements added renders the children inside the cell. The render priority is: children > raw HTML > text. Useful for cells that contain links, buttons, or formatted markup.

var table = content.Add(new Table(CoreCss.BorderTable));
table.AddHeaderRow("User", "Action");

var row = table.AddEmptyRow();
row.Add(new Td("Ada Lovelace"));
var actionCell = row.Add(new Td());
actionCell.Add(new A("/users/ada/edit", "Edit"));
actionCell.Add(new Text(" | "));
actionCell.Add(new A("/users/ada/delete", "Delete"));
Renders as:
User Action
Ada Lovelace Edit | Delete

SimpleTable: Auto-Generated Columns

The fastest possible data-bound table. Pass a DataTable (or any DbResults that exposes one) and nothing else — SimpleTable auto-generates one column per DataColumn, using the column name as both the field reference and the header. Pair with CoreCss.BorderTable (or any CSS class) for visible styling, or pass no class for an unstyled table.

DataTable data = SomeAdapter.LoadData(); // returns DataTable

// Zero column config — every DataColumn becomes a Th with the column name
content.Add(new SimpleTable(data, CoreCss.BorderTable));
Renders as:
employee_id employee_name role hire_date
101 Ada Lovelace Engineer 12/10/1843 12:00:00 AM
102 Grace Hopper Architect 6/1/1944 12:00:00 AM
103 Alan Turing Researcher 5/28/1936 12:00:00 AM

Use this when you want a quick read-only view of whatever the database returned — admin tools, debug pages, or any spot where the raw column names are good enough as headers.

SimpleTable: Named Columns and Formatters

When you want control over which columns appear, what their headers say, or how values are formatted, use DefineColumn. The column order in your DefineColumn calls determines the column order in the rendered table — fields not listed are excluded.

var simple = new SimpleTable(data, CoreCss.BorderTable)
    .DefineColumn("employee_name", "Name")
    .DefineColumn("role",          "Role")
    .DefineColumn("hire_date",     "Hired",
               value => Convert.ToDateTime(value).ToString("yyyy-MM-dd"))
    .DefineColumn("salary",        "Salary",
               value => $"${Convert.ToDecimal(value):N2}");

content.Add(simple);
Renders as:
Name Role Hired Salary
Ada Lovelace Engineer 1843-12-10 $92,500.00
Grace Hopper Architect 1944-06-01 $118,000.00
Alan Turing Researcher 1936-05-28 $105,500.00

Row Formatters: Multi-Field Cells

DefineColumn(header, rowFormatter) gives the formatter the entire DataRow, so you can build a cell from multiple fields. AllowHtml is set automatically — return any markup you need.

var simple = new SimpleTable(data, CoreCss.BorderTable)
    .DefineColumn("Name", row => $"<strong>{row["last_name"]}</strong>, {row["first_name"]}")
    .DefineColumn("salary", "Salary",
               value => $"${Convert.ToDecimal(value):N2}");

content.Add(simple);
Renders as:
Name Salary
Lovelace, Ada $92,500.00
Hopper, Grace $118,000.00

Pulling Formatters Into Helper Methods

Inline lambdas are fine for one-liners, but once a formatter has any real logic — a permission check, a multi-branch return, anything that takes more than one expression — extract it into a private method and pass it as a method-group reference. DefineColumn happily accepts any Func<DataRow, string>, including a method group, which reads cleaner than a multi-line lambda nested inside a fluent chain.

// Inline lambda — fine when the cell is a single expression
.DefineColumn("Schema", row =>
    $"<a href=\"/Team/DD/TableList?DB={row["db_name"]}&Schema={row["schema_name"]}\">{row["schema_name"]}</a>")

// Method group — preferred when the cell needs branching, permission checks,
// or anything you'd want to read top-to-bottom rather than parse as one expression
.DefineColumn("Action", BuildEditLink)

private string BuildEditLink(DataRow row)
{
    string owners = $"{row["owners"]}";
    bool canEdit = owners.Contains(MudID) || _userAdmin;
    if (!canEdit)
        return "";

    return $"<a href=\"/Team/DD/SchemaEdit?DB={row["db_name"]}&Schema={row["schema_name"]}\">Edit</a>";
}

The method captures page-level state (MudID, _userAdmin, role flags) just like a closure would, but it gets a real name, a real signature, and the option to be unit-tested in isolation. The fluent column chain stays scannable — column declarations read as a list of (header, source) pairs, with the actual rendering logic factored away. Use this pattern any time the formatter is more than ~40 characters of body, OR contains conditional logic, OR appears in more than one column.

SimpleTable: Adding Action Columns (AddColumn / InsertColumn)

Many tables need columns that aren't in the DataTable schema — Edit/Delete links, checkboxes, status badges built from multiple fields. AddColumn tacks a column onto the end of the rendered table; InsertColumn places one at a specific position (use index 0 to prepend). Both work with auto-generated columns (no DefineColumn needed) or on top of explicit DefineColumn calls, so you can mix and match.

Addolumn: An Action Column on the Right

The common case: let SimpleTable auto-generate the data columns from the DataTable, then tack an Edit link on the end. No DefineColumn calls — the schema columns appear first, in the order the DataTable provides them, and the appended column comes last.

content.Add(new SimpleTable(data, CoreCss.BorderTable)
    .AddColumn("Action", row =>
        $"<a href=\"/users/{row["employee_id"]}/edit\">Edit</a>"));
Renders as:
employee_id employee_name role Action
101 Ada Lovelace Engineer Edit
102 Grace Hopper Architect Edit
103 Alan Turing Researcher Edit

InsertColumn: A Column at the Start (or Any Position)

InsertColumn(0, ...) puts the column at the front — useful for a checkbox or select column on a list page. Any non-negative index works; an index past the end clamps to the end. The example below combines both methods: a Select column at the front and an Action column on the right.

content.Add(new SimpleTable(data, CoreCss.BorderTable)
    .InsertColumn(0, "Select", row =>
        $"<input type=\"checkbox\" name=\"sel_{row["employee_id"]}\" />")
    .AddColumn("Action", row =>
        $"<a href=\"/users/{row["employee_id"]}/edit\">Edit</a>"));
Renders as:
Select employee_id employee_name role Action
101 Ada Lovelace Engineer Edit
102 Grace Hopper Architect Edit
103 Alan Turing Researcher Edit

Combining with DefineColumn

AddColumn and InsertColumn also layer on top of DefineColumn. Use this when you want explicit control over which data columns appear AND an extra action column — DefineColumn declares the data columns in order, then Append/Insert splice in the extras.

content.Add(new SimpleTable(data, CoreCss.BorderTable)
    .DefineColumn("employee_name", "Name")
    .DefineColumn("role",          "Role")
    .AddColumn("Action", row =>
        $"<a href=\"/users/{row["employee_id"]}/edit\">Edit</a>"));
Renders as:
Name Role Action
Ada Lovelace Engineer Edit
Grace Hopper Architect Edit
Alan Turing Researcher Edit

Insert Order Semantics

InsertColumn follows List<T>.Insert semantics — each call physically inserts at the given index of the current list, so later calls at the same index push earlier inserts forward. To insert multiple columns at the front in a specific left-to-right order, use sequential indexes:

// Result: [A, B, ...schema columns]
.InsertColumn(0, "A", ...)
.InsertColumn(1, "B", ...)

// Result: [B, A, ...schema columns]  — A got pushed right when B was inserted at 0
.InsertColumn(0, "A", ...)
.InsertColumn(0, "B", ...)

Empty State Message

If the DataTable has zero rows, SimpleTable renders a paragraph with the empty message instead of an empty table. Customize via WithEmptyMessage.

var simple = new SimpleTable(emptyData, CoreCss.BorderTable)
    .WithEmptyMessage("No employees found for the selected filters.")
    .DefineColumn("employee_name", "Name");
Renders as:

No employees found for the selected filters.

Scroll Behavior: Flow vs Excel

Flow (default) lets the table size to its container; cells wrap their text naturally and the page handles all scrolling. Excel mode is for wide data tables: cells switch to single-line nowrap, the table sizes to its widest content, and a wrapping div provides horizontal scrolling so the page layout doesn't break when the table is wider than its container. Vertical scroll is still the page's in Excel mode — to put the vertical scrollbar on the table itself, combine with WithStickyHeader (below).

// Excel mode: wide-data scrolling with single-line cells
var simple = new SimpleTable(data, CoreCss.BorderTable)
    .WithScrollBehavior(TableScrollBehavior.Excel)
    .DefineColumn("employee_name", "Name")
    .DefineColumn("role",          "Role")
    .DefineColumn("hire_date",     "Hired");
Renders as:

Scroll horizontally inside the box. Vertical scroll is the page's — to see that, scroll the page itself.

ID Name Role Department Location Hired Notes
1000 Edsger Hopper Engineer Infrastructure London, UK 2017-07-08 Internal-tools focus: built the deploy dashboard everyone uses, no fanfare.
1001 Donald Hopper Tech Lead Data Engineering Cambridge, MA 2019-11-05 Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1002 Linus Turing Researcher Platform Bangalore, IN 2020-07-13 On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1003 Grace Lovelace Tech Lead Developer Tools Mountain View, CA 2015-06-04 Internal-tools focus: built the deploy dashboard everyone uses, no fanfare.
1004 Grace Stroustrup Tech Lead Infrastructure London, UK 2015-05-26 Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1005 Donald Hoare Engineer Infrastructure Bangalore, IN 2022-03-04 On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1006 Grace Lovelace Engineer Security Zurich, CH 2016-02-08 Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1007 Barbara Turing Researcher Platform London, UK 2020-10-16 Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1008 Ada Hamilton Tech Lead Security Cambridge, MA 2015-02-16 On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1009 Alan Lovelace Tech Lead Platform Mountain View, CA 2015-02-12 Internal-tools focus: built the deploy dashboard everyone uses, no fanfare.
1010 Margaret Knuth Architect Platform Bangalore, IN 2015-02-15 Mentors junior team members on distributed systems patterns and interview prep.
1011 Ada Dijkstra Engineer Developer Tools Bangalore, IN 2019-06-03 On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1012 Edsger Lovelace Architect Platform Mountain View, CA 2022-08-05 Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1013 Margaret Turing Principal Engineer Infrastructure Zurich, CH 2015-03-09 Cross-functional partner with Product on the new metrics surface — strong communicator.
1014 Bjarne Torvalds Tech Lead Platform London, UK 2019-05-02 Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1015 Barbara Hopper Engineer Data Engineering Mountain View, CA 2019-11-30 Cross-functional partner with Product on the new metrics surface — strong communicator.
1016 Barbara Dijkstra Tech Lead Data Engineering Mountain View, CA 2024-06-26 Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1017 Ada Torvalds Principal Engineer Platform Cambridge, MA 2017-07-18 Mentors junior team members on distributed systems patterns and interview prep.
1018 Alan Knuth Architect Platform Bangalore, IN 2016-02-29 Mentors junior team members on distributed systems patterns and interview prep.
1019 Linus Torvalds Tech Lead Developer Tools London, UK 2015-05-28 Cross-functional partner with Product on the new metrics surface — strong communicator.
1020 Tony Turing Principal Engineer Developer Tools Cambridge, MA 2020-03-23 Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1021 Edsger Hopper Senior Engineer Platform London, UK 2017-03-18 Cross-functional partner with Product on the new metrics surface — strong communicator.
1022 Bjarne Liskov Engineer Developer Tools London, UK 2017-10-28 Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1023 Donald Liskov Engineer Infrastructure Cambridge, MA 2023-06-11 On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1024 Linus Stroustrup Architect Infrastructure Bangalore, IN 2022-10-07 Mentors junior team members on distributed systems patterns and interview prep.
1025 Grace Lovelace Senior Engineer Developer Tools Zurich, CH 2018-11-15 Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1026 Margaret Hopper Principal Engineer Developer Tools Bangalore, IN 2016-08-27 On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1027 Donald Dijkstra Principal Engineer Developer Tools Zurich, CH 2017-09-08 On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1028 Donald Hoare Senior Engineer Developer Tools Zurich, CH 2016-10-25 Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1029 Alan Hoare Researcher Platform Mountain View, CA 2022-09-18 Cross-functional partner with Product on the new metrics surface — strong communicator.

WithMaxLines(N) relaxes the strict single-line rule when you want N lines per cell instead. Only meaningful in Excel mode. Range 1-5.

// Allow up to 2 lines per cell instead of strict single-line
var simple = new SimpleTable(data, CoreCss.BorderTable)
    .WithScrollBehavior(TableScrollBehavior.Excel)
    .WithMaxLines(2)
    .DefineColumn("employee_name", "Name");
Renders as:

The Notes column has long text. Without WithMaxLines it would render as a very wide single line; here it wraps to 2 lines and clips.

ID Name Role Department Location Hired Notes
1000
Edsger Hopper
Engineer
Infrastructure
London, UK
2017-07-08
Internal-tools focus: built the deploy dashboard everyone uses, no fanfare.
1001
Donald Hopper
Tech Lead
Data Engineering
Cambridge, MA
2019-11-05
Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1002
Linus Turing
Researcher
Platform
Bangalore, IN
2020-07-13
On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1003
Grace Lovelace
Tech Lead
Developer Tools
Mountain View, CA
2015-06-04
Internal-tools focus: built the deploy dashboard everyone uses, no fanfare.
1004
Grace Stroustrup
Tech Lead
Infrastructure
London, UK
2015-05-26
Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1005
Donald Hoare
Engineer
Infrastructure
Bangalore, IN
2022-03-04
On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1006
Grace Lovelace
Engineer
Security
Zurich, CH
2016-02-08
Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1007
Barbara Turing
Researcher
Platform
London, UK
2020-10-16
Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1008
Ada Hamilton
Tech Lead
Security
Cambridge, MA
2015-02-16
On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1009
Alan Lovelace
Tech Lead
Platform
Mountain View, CA
2015-02-12
Internal-tools focus: built the deploy dashboard everyone uses, no fanfare.
1010
Margaret Knuth
Architect
Platform
Bangalore, IN
2015-02-15
Mentors junior team members on distributed systems patterns and interview prep.
1011
Ada Dijkstra
Engineer
Developer Tools
Bangalore, IN
2019-06-03
On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1012
Edsger Lovelace
Architect
Platform
Mountain View, CA
2022-08-05
Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1013
Margaret Turing
Principal Engineer
Infrastructure
Zurich, CH
2015-03-09
Cross-functional partner with Product on the new metrics surface — strong communicator.
1014
Bjarne Torvalds
Tech Lead
Platform
London, UK
2019-05-02
Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1015
Barbara Hopper
Engineer
Data Engineering
Mountain View, CA
2019-11-30
Cross-functional partner with Product on the new metrics surface — strong communicator.
1016
Barbara Dijkstra
Tech Lead
Data Engineering
Mountain View, CA
2024-06-26
Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1017
Ada Torvalds
Principal Engineer
Platform
Cambridge, MA
2017-07-18
Mentors junior team members on distributed systems patterns and interview prep.
1018
Alan Knuth
Architect
Platform
Bangalore, IN
2016-02-29
Mentors junior team members on distributed systems patterns and interview prep.
1019
Linus Torvalds
Tech Lead
Developer Tools
London, UK
2015-05-28
Cross-functional partner with Product on the new metrics surface — strong communicator.
1020
Tony Turing
Principal Engineer
Developer Tools
Cambridge, MA
2020-03-23
Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1021
Edsger Hopper
Senior Engineer
Platform
London, UK
2017-03-18
Cross-functional partner with Product on the new metrics surface — strong communicator.
1022
Bjarne Liskov
Engineer
Developer Tools
London, UK
2017-10-28
Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1023
Donald Liskov
Engineer
Infrastructure
Cambridge, MA
2023-06-11
On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1024
Linus Stroustrup
Architect
Infrastructure
Bangalore, IN
2022-10-07
Mentors junior team members on distributed systems patterns and interview prep.
1025
Grace Lovelace
Senior Engineer
Developer Tools
Zurich, CH
2018-11-15
Drove the bundle-size reduction effort, shipped a 40% improvement to first paint.
1026
Margaret Hopper
Principal Engineer
Developer Tools
Bangalore, IN
2016-08-27
On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1027
Donald Dijkstra
Principal Engineer
Developer Tools
Zurich, CH
2017-09-08
On-call rotation for the ingest pipeline. Handles after-hours pages calmly.
1028
Donald Hoare
Senior Engineer
Developer Tools
Zurich, CH
2016-10-25
Owns the migration from legacy auth middleware to OAuth2 — multi-quarter project.
1029
Alan Hoare
Researcher
Platform
Mountain View, CA
2022-09-18
Cross-functional partner with Product on the new metrics surface — strong communicator.

Sticky Header: WithStickyHeader

Pin the header row in place while the body scrolls — the same effect as Excel's "Freeze Top Row". The table is wrapped in a scroll container so the scrollbar belongs to the table, not the page. This is what you want for long lists embedded in a dashboard panel: the surrounding chrome (filters, KPIs, page header) stays visible while the user scrolls through hundreds of rows.

// Sticky header that fills its parent panel
var simple = new SimpleTable(data, CoreCss.BorderTable)
    .WithStickyHeader()
    .DefineColumn("employee_name", "Name")
    .DefineColumn("role",          "Role");

By default the wrapper uses flex:1 to fill its parent container vertically — this works when the table sits inside a panel-fill or panel-col. Pass any CSS length to override with a fixed height instead.

Renders as:

The demo box below is a 250px-tall flex column, mimicking a dashboard panel. The table fills it via flex:1; scroll inside the box to see the header stay put.

// Fixed-height scroll region (any CSS length is accepted)
.WithStickyHeader("600px")
.WithStickyHeader("70vh")
.WithStickyHeader("calc(100vh - 200px)")
Renders as:

Same data, fixed 200px height — no flex parent needed.

Bounded Height: WithMaxHeight

If you want a scrollable table-scoped scrollbar but DON'T want the sticky-header treatment (header scrolls with the body), use WithMaxHeight instead. Same wrapper, no sticky positioning, no border-collapse change. Useful when the regular border-table look matters more than keeping headers visible — for example, a short reference table you want to bound at 300px without changing its appearance.

// Plain bordered table, capped at 200px tall
var simple = new SimpleTable(data, CoreCss.BorderTable)
    .WithMaxHeight("200px")
    .DefineColumn("employee_name", "Name")
    .DefineColumn("role",          "Role");
Renders as:

Same table as the sticky-fixed demo above, but the header scrolls with the body. Notice the borders use border-collapse:collapse like a normal border-table — no visual difference from a non-scrolled border-table other than the scroll behavior.

ID Name Role
1000 Edsger Hopper Engineer
1001 Donald Hopper Tech Lead
1002 Linus Turing Researcher
1003 Grace Lovelace Tech Lead
1004 Grace Stroustrup Tech Lead
1005 Donald Hoare Engineer
1006 Grace Lovelace Engineer
1007 Barbara Turing Researcher
1008 Ada Hamilton Tech Lead
1009 Alan Lovelace Tech Lead
1010 Margaret Knuth Architect
1011 Ada Dijkstra Engineer
1012 Edsger Lovelace Architect
1013 Margaret Turing Principal Engineer
1014 Bjarne Torvalds Tech Lead
1015 Barbara Hopper Engineer
1016 Barbara Dijkstra Tech Lead
1017 Ada Torvalds Principal Engineer
1018 Alan Knuth Architect
1019 Linus Torvalds Tech Lead
1020 Tony Turing Principal Engineer
1021 Edsger Hopper Senior Engineer
1022 Bjarne Liskov Engineer
1023 Donald Liskov Engineer
1024 Linus Stroustrup Architect
1025 Grace Lovelace Senior Engineer
1026 Margaret Hopper Principal Engineer
1027 Donald Dijkstra Principal Engineer
1028 Donald Hoare Senior Engineer
1029 Alan Hoare Researcher

WithMaxHeight composes with WithStickyHeader and WithScrollBehavior(Excel) the same way maxHeight does as an argument to WithStickyHeader. The two methods can be chained in either order — the explicit value wins, an unspecified argument leaves any prior value intact.

Composing with Excel Mode

WithStickyHeader and WithScrollBehavior(Excel) are orthogonal and fully composable. The sticky wrapper handles both X and Y scroll, so when both are active you get nowrap single-line cells, horizontal scroll, vertical scroll, and a sticky header all together. This is the closest match to a native Excel grid — the table-scoped scrollbars combined with frozen headers.

// Wide table + sticky header — full Excel-grid feel
var simple = new SimpleTable(data, CoreCss.BorderTable)
    .WithScrollBehavior(TableScrollBehavior.Excel)
    .WithStickyHeader()
    .DefineColumn("employee_name", "Name")
    .DefineColumn("role",          "Role")
    .DefineColumn("department",    "Department")
    .DefineColumn("hire_date",     "Hired");
Renders as:

Scroll right (wide data, X-scroll) AND down (sticky header, Y-scroll) — both scrollbars belong to the table, the page chrome stays put.

Why border-collapse Matters

CSS sticky positioning interacts badly with border-collapse:collapse (the default). Collapsed borders are shared between adjacent cells, so the bottom border of a sticky <th> is partly owned by the first body <td> — meaning that border scrolls away with the body, leaving the header without a bottom edge. WithStickyHeader switches the table to border-collapse:separate and re-draws the borders so each visible line is owned by exactly one cell. You don't need to do anything to enable this — it's automatic when you add the sticky-header class. But it's why the visual border behavior changes slightly compared to a plain border-table.

← Back to Guide

Please wait...