Making an HTML Table Cell Scrollable

26.03.25    web technologies    markup

Sometimes you just want to put too much text into a single table cell. To avoid breaking the layout, you would want to make the overflowing content of the cell scrollable. But it turns out, that is not so easy.

We start with our first attempt at the CSS:

td.scrollable {
    overflow: auto;
}

Sadly, this does not work at all! By default, td elements have no overflow because the surrounding table's column and row grow to accommodate the content.

We could solve this by giving an explicit, fixed width and height for our table cell. But then we lose the main purpose of having a table: the width and height of a cell being decided based on the content of all cells in the column or row respectively. The fixed cell would not shrink, even if its content was small. And it would not grow to use the available space when the width of the column or height of the row was pushed by other (non-scrollable) cells.

So it turns out, we have to introduce a new child element into the markup. It's a div inside the td which has a .scrollable class (which will have the overflow: auto attribute):

<td>
    <div class="scrollable">
        Lorem ipsum
    </div>
</td>

For this to make sense, we have to make sure that the div fills out its parent exactly, regardless of content. That sounds like the perfect use case for display: flex on the surrounding td. Except, that's not allowed! The element td has a special display value of table-cell that is necessary for the function of the table. This cannot be overridden (without breaking the table).

At the same time, the contained div must not exert pressure on the size of its parent td. The size of the div must be restricted to some sensible default / minimum1. And only when the td grows because of the content of other cells in the same row or column, then div should grow with it.

We have to be a little bit creative here. First, we have to specify a fixed size (CSS width and height) for the div of, for example, 5em. Then we define a dynamic, percentage-based minimum size (minimum-width and minimum-height) of 100%. Initially the div takes on the specified size. When other content in the same row or column forces the td to be larger, then the minimum size of the div takes precedence, and it enlarges to keep filling out the td.

.scrollable {
    overflow: auto;
    width: 5em;
    height: 5em;
    min-width: 100%;
    min-height: 100%;
}

Note that the specification of size and minimum size is counter to the intuitive arrangement: It reads like "The size of .scrollable should be 5em, but at least 100% of its parent." Although the logical roles of size and minimum size are symmetrical (the actual size of the element is larger than both the size and minimum size), swapping the values in the rules does not work. The rules only function, because of the exact order of operations in the size calculation and layout process.

These rules have the intended effect for the width. But they are not enough yet for the height. Percentage-based heights are only allowed if the parent has a well-defined height itself (for example by being fixed with CSS rules in terms of fixed unit like em or px). This is necessary to avoid cyclic constraints. Thus, if we only have the rules as above, the browser will ignore the min-height rule because the surrounding td has a dynamic height.

Luckily, we can cheat the system to fix this. We can specify an absurdly low height for the td. It will still grow to accommodate its actual content. And the 100% rule referencing its height will be active, because now the height is "fixed". The fact, that this fixed height is pointless, turns out not to matter.

td { height: 1px; padding: 0 }

It works! Except, it only does in Chromium-based browsers. For Firefox, we have to add a different rule. Firefox does not grow the td for us. There the td stays restricted to our absurd 1px height. Instead, we have to defer the "fixedness" of the td's height to its parent, tr. And at the tr to its parent again, which would be the table. At that stage, it stops making sense. It's just what's necessary to make it work.2

We can add the Firefox-specific rules inside a special query. The query checks for the support of specific CSS attributes. The contained rules are activated only in supporting browsers. By checking for the support of a Firefox-specific (-moz-) attribute, we can precisely target the rules to Firefox:

@supports( -moz-appearance:none ) {
    td, tr {
        height: 100% !important;
    }
}

That's it. Now it works. We have a scrollable div that transparently fills out its parent td, effectively making it a scrollable table cell.

Demo

This table contains 9 cells. The 8 surrounding cells are scrollable. The inner cell with red text is not scrollable and forces its row and column to grow. Resize your browser window to see how the table reacts to different sizing conditions.

Correct scrollable cells:

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.

Failure to grow

Here we just restrict the size of the scrollable div. Look how much wasted space there is and how misaligned the cells look.

.scrollable {
    overflow: auto;
    width: 5em;
    height: 5em;
    /*
    min-width: 100%;
    min-height: 100%;
    */
}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.

Failure to restrict size

Here we do not restrict the cell size first with a fixed value. The tds grow based on their content rendering the overflow useless. There are no scrollbars here.

.scrollable {
    overflow: auto;
    /*
    width: 5em;
    height: 5em;
    */
    min-width: 100%;
    min-height: 100%;
}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam dictum pharetra lorem a luctus. Morbiinterdum felis vel auctor mollis.

  1. This can also be 1px if you don't actually need a minimum. But defining a value is still necessary as explained below and can be seen in the demo. 

  2. If someone has a coherent explanation for this behaviour, please let me know! Also, this behaviour is specific to Firefox. Using the Firefox rules in Chrome does not work either.