The Engineering Lesson I Learned From Fixing a “Simple” Table

Share this post on:

I thought I was fixing a small accessibility issue.

We had tables rendering with only <tr> and <td>. Visually, everything looked fine. Bold text made headers stand out. The layout made sense.

So I assumed this would be quick.

It wasn’t.


When “looks right” isn’t actually right

Here’s what we were working with:

<table>
  <tr>
    <td><b>Year Title</b></td>
    <td><b>2024</b></td>
    <td><b>2025</b></td>
  </tr>
  <tr>
    <td><b>Assets</b></td>
  </tr>
  <tr>
    <td>Money</td>
    <td>$xxx</td>
    <td>$yyy</td>
  </tr>
</table>

To a human, this is easy to understand.

To a screen reader, it’s just a grid of cells with no hierarchy.

No headers. No relationships. No structure.

And that’s where the real problem was.


Lesson #1: Don’t confuse styling with meaning

Bold text made it look like we had headers.

But HTML doesn’t care about how things look—it cares about what they are.

  • <td> = data
  • <th> = header

If everything is a <td>, nothing is a header.

That means assistive technologies can’t:

  • associate values with headers
  • navigate the table correctly
  • understand context

The UI was clear. The structure wasn’t.


Lesson #2: Don’t assume more than the data tells you

My first instinct was to “fix everything”:

  • turn the first cell into a caption
  • restructure the entire table

But I had to pause.

Just because something looks like a caption doesn’t mean it is one.

So instead, I made the smallest safe improvements:

  • convert the first row into <thead>
  • convert its cells into <th>
  • leave everything else as-is unless I was sure

The goal wasn’t perfection—it was correctness without guessing.


Lesson #3: Real-world HTML is messy

I expected all tables to look like this:

<table>
  <tr>...</tr>
</table>

But some looked like this:

<table>
  <tbody>
    <tr>...</tr>
  </tbody>
</table>

My code initially missed those rows entirely.

Which led to a bug where I returned null
and accidentally removed the entire table.

That one line broke everything.


Lesson #4: Small implementation details matter more than you think

In the parser I was using:

  • undefined → keep the node
  • null → remove the node

That difference isn’t obvious until it is.

It’s the kind of thing that doesn’t show up in the UI right away—but can quietly break content.


Lesson #5: Performance fixes can cancel themselves out

I optimized the parsing with useMemo so it wouldn’t run on every render.

Then I used random keys.

Which forced React to re-mount everything anyway.

Same result. Just more complicated.

The fix was simple:

  • use stable keys tied to the data

Lesson #6: Defensive code is not overengineering

I added a filter to only process <td> and <th> elements.

At first glance, it looked unnecessary.

But parsed HTML often includes:

  • whitespace nodes
  • unexpected elements

Without that filter, I’d end up processing things that weren’t actually cells.

When your input isn’t fully controlled, defensive code isn’t optional.


What I actually changed

  • Introduced <thead> and <tbody> for structure
  • Converted header cells to <th>
  • Preserved existing content without over-assuming intent
  • Memoized parsing to avoid unnecessary work
  • Replaced unstable keys with deterministic ones

The real takeaway

This wasn’t about tables.

It was about alignment.

Does your code structure reflect the meaning of what you’re building?

Because when it doesn’t, the UI might still look fine—but the experience breaks for the people who rely on that structure the most.

And those issues are easy to miss if you’re only looking at the screen.

Share this post on:

Leave a Reply

Your email address will not be published. Required fields are marked *