Short answer: A data table in a mockup is judged on scanning, so cut the columns down to the ones a reader compares, build the header as its own block, repeat one row height, and show the loading and empty states alongside the loaded one.
List the columns a reader compares, not the fields you have
Start from the decision the table supports, not from the data model. A list of the fields you have is a record dump, and a record dump cannot be scanned in a static image. Ask what a reader compares when they look at this table: which project, who owns it, when it is due, and whether it is late. Those four are the columns.
Write the columns down before you build anything, and mark the one that is the reason the table exists. That column goes first and gets the most weight, because a table where every column is equally weighted is a table nobody reads.
Keep the count low. Five or six columns can be read across a screen at a glance, and each extra column narrows all the others. If a column has to stay, put it last and give it the least space.
- Columns come from the comparison, not from the data model
- The reason the table exists gets the first column and the most weight
- Five or six columns before the rest get too narrow
Note: A mockup table has to be read as a picture, not used as a spreadsheet. If a column only matters when you open a record, it belongs on the detail screen rather than in the table.
Design the header row as its own block
The header row is a separate problem from the body rows, and treating it as one is why tables end up with squashed headings. The header is labels rather than data, so it can be a fixed block above the rows while the rows move under it. Build it as its own container and give it the same padding as the cells so the columns line up.
Label each column with the name a reader would use, not the name in the database. Owner rather than assignee, Due rather than due_date. If a header needs a sentence to explain it, it belongs in the row instead.
Right-align numbers and dates and left-align names and text. It sounds trivial, and it is the difference between a table that reads as a grid and a table that reads as a list of sentences.
- The header is a fixed block above the rows
- Header labels match the row content, column for column
- Numbers and dates right-aligned, names and text left-aligned
Set a row height and repeat it
One row height, repeated. A row that is the same height in every frame of the same table is what makes a table look designed, and it is a number you can check. Build one row, set its contents in a horizontal auto-layout with a fixed gap, and duplicate that row for each record.
The row is a horizontal container holding the cells. Each cell is a vertical stack of a primary line and, if you need it, a secondary line in smaller type. That two-line cell lets a table carry an owner and a status without doubling the number of columns.
Use the List item component for a row, or a row of Text elements in a horizontal container. The Table component is in the palette too if you want the whole thing as one object, and a vertical auto-layout container holds the rows so they stack evenly.
- One fixed row height, duplicated for every record
- A row is a horizontal auto-layout with a fixed gap
- A cell is a primary line plus an optional secondary line
- A vertical auto-layout container stacks the rows
Note: The Table, List item, Badge and Avatar component kinds are all in the App Builder palette, so none of this needs to be drawn by hand. A row is a container that holds children, and the maximum nesting depth in the editor is 8.
Decide where row actions live
A table of a hundred rows with a button on every one of them is a wall of buttons. Put the actions at the end of the row, in the last column, as a single menu or a single link, so the row still reads from left to right. If the action is the main thing the reader does, then that column is the reason the table exists and it goes first instead.
Some rows need a state rather than an action: a Badge for a status, an Avatar for a person, a Switch for a toggle. Those are the component kinds that live inside a cell, and mixing them with a menu in the same column is a sign that the table is doing two jobs at once.
Whatever you choose, make the destructive action harder to reach than the ordinary one. A delete sitting in the same place as an edit, at the same size and the same weight, will be pressed by accident eventually.
- Actions last, in one column, so the row reads left to right
- Badge for status, Avatar for a person, Switch for a toggle
- Destructive actions placed further away than ordinary ones
Show the loading and empty states too
A table mockup with only the loaded state is half a design. Show the loading state and the empty state next to the loaded one, because a real table spends a surprising amount of its life in one of the other two. A Skeleton component stands in for the loading rows, with the same row height and the same column positions, so the page does not jump when the data arrives.
The empty state for a table is different from the empty state for a list. There is no content to describe, so the message has to explain the filter or the search that produced the nothing, and offer a way to change it. The empty state guide covers the structure of that block in more detail.
Pagination belongs under the table, with a row count next to it. The Pagination component is in the palette, and the count tells the reader whether they are looking at everything or at one page, which is the question they will ask anyway.
- A Skeleton row set matches the real row height and columns
- The empty table message explains the filter and offers a way out
- Pagination sits under the table with a row count beside it
Note: The loaded, loading and empty versions are three separate frames in App Builder. Keeping them as three frames rather than one frame with three states is what lets you review them against each other.
Match the table's width to the data you have
A table with six columns stretched across a 1440-pixel frame is a table of small text and long gaps. Set the table width to what the data needs and let the frame around it deal with the rest. If the columns are narrow, the table stays narrow and sits inside the content area rather than filling it.
In a narrow frame, reduce the columns rather than shrinking them. On a 390-pixel-wide frame, three columns of real text read better than six columns of truncated text, and a list of cards is often the better answer on a phone. The complete Canvasola guide covers the app builder views these tables sit in.
Whatever the width, the vertical rhythm should be identical everywhere. One row height, one gap between the header and the first row, one padding value inside each cell, and the table reads as a system. The design system guide covers keeping that consistent across every screen.
- Table width follows the data, not the frame
- Fewer columns on a narrow frame rather than smaller text
- One row height, one header gap, one cell padding
Note: A table inside a wider layout is covered in the dashboard layout guide.
A checklist you can reuse
- The columns were chosen from the comparison a reader makes, not from the data model
- The most important column is first and has the most weight
- The header is its own block, with labels that match the row content
- Every row has the same height, set as a number and repeated
- Row actions sit in one column, with destructive actions placed further away
- Loaded, loading and empty states all exist as separate frames
- Pagination and a row count sit below the table
Common mistakes to avoid
- Showing every field you have. A table of record fields cannot be scanned. Keep the columns a reader compares and move the rest to a detail screen.
- Putting a button in every row. A wall of buttons hides the data. Put one action in the last column so the row still reads from left to right.
- Letting the header scroll with the rows. A header that belongs to the rows travels with them, and the column labels disappear as soon as the list is long. Build the header as its own block above the rows.
- Designing only the loaded state. Real tables spend a lot of their time empty or loading. Model both, and check that the Skeleton rows match the real row height.
- Stretching a narrow table across a wide frame. Six columns across 1440 pixels is small text and long gaps. Set the table to the width the data needs and let the page hold the rest.
Frequently asked questions
How many columns should a data table have?
Five or six is the practical limit for a table read at a glance. Each extra column narrows all the others and pushes more text into truncation. If the decision needs more than six pieces of information, a detail view is usually the better place for the rest.
How do I show a loading table in a mockup?
Use the Skeleton component for the rows and copy the real row height and column positions, so nothing moves when the data arrives. Give the header its real labels rather than skeletons, because the header is the part the reader is looking for while the rows load.
Should the header be part of the scrolling rows?
In a static mockup it does not matter mechanically, but keeping the header as a separate block above the rows is what tells the reader which columns they are looking at. It also makes the column alignment obvious, because the header and the cells are two blocks that have to line up.
What is the difference between the Table and List item components?
The Table component gives you the whole table as one object, which is quick but harder to control row by row. The List item component is a single row that you place and duplicate, so each cell can have its own alignment and padding. Many tables are built from List items in a vertical auto-layout container.
Build the header, one row, and the two states around them.
Open App Builder, place a Table or a List item on a web frame, fix the row height, and add the loading and empty frames next to it.
This guide is part of the app builder guides collection. For the wider product tour, read the complete Canvasola guide, and keep the keyboard shortcut cheat sheet open while you work.