{"id":41,"date":"2026-04-16T23:28:03","date_gmt":"2026-04-16T23:28:03","guid":{"rendered":"https:\/\/vanianettleford.com\/?p=41"},"modified":"2026-04-16T23:28:03","modified_gmt":"2026-04-16T23:28:03","slug":"the-engineering-lesson-i-learned-from-fixing-a-simple-table","status":"publish","type":"post","link":"https:\/\/vanianettleford.com\/?p=41","title":{"rendered":"The Engineering Lesson I Learned From Fixing a \u201cSimple\u201d Table"},"content":{"rendered":"\n<p>I thought I was fixing a small accessibility issue.<\/p>\n\n\n\n<p>We had tables rendering with only <code>&lt;tr&gt;<\/code> and <code>&lt;td&gt;<\/code>. Visually, everything looked fine. Bold text made headers stand out. The layout made sense.<\/p>\n\n\n\n<p>So I assumed this would be quick.<\/p>\n\n\n\n<p>It wasn\u2019t.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">When \u201clooks right\u201d isn\u2019t actually right<\/h2>\n\n\n\n<p>Here\u2019s what we were working with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;table>\n  &lt;tr>\n    &lt;td>&lt;b>Year Title&lt;\/b>&lt;\/td>\n    &lt;td>&lt;b>2024&lt;\/b>&lt;\/td>\n    &lt;td>&lt;b>2025&lt;\/b>&lt;\/td>\n  &lt;\/tr>\n  &lt;tr>\n    &lt;td>&lt;b>Assets&lt;\/b>&lt;\/td>\n  &lt;\/tr>\n  &lt;tr>\n    &lt;td>Money&lt;\/td>\n    &lt;td>$xxx&lt;\/td>\n    &lt;td>$yyy&lt;\/td>\n  &lt;\/tr>\n&lt;\/table>\n<\/code><\/pre>\n\n\n\n<p>To a human, this is easy to understand.<\/p>\n\n\n\n<p>To a screen reader, it\u2019s just a grid of cells with no hierarchy.<\/p>\n\n\n\n<p>No headers. No relationships. No structure.<\/p>\n\n\n\n<p>And that\u2019s where the real problem was.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lesson #1: Don\u2019t confuse styling with meaning<\/h2>\n\n\n\n<p>Bold text made it <em>look<\/em> like we had headers.<\/p>\n\n\n\n<p>But HTML doesn\u2019t care about how things look\u2014it cares about what they <em>are<\/em>.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>&lt;td><\/code> = data<\/li>\n\n\n\n<li><code>&lt;th><\/code> = header<\/li>\n<\/ul>\n\n\n\n<p>If everything is a <code>&lt;td&gt;<\/code>, nothing is a header.<\/p>\n\n\n\n<p>That means assistive technologies can\u2019t:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>associate values with headers<\/li>\n\n\n\n<li>navigate the table correctly<\/li>\n\n\n\n<li>understand context<\/li>\n<\/ul>\n\n\n\n<p>The UI was clear. The structure wasn\u2019t.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lesson #2: Don\u2019t assume more than the data tells you<\/h2>\n\n\n\n<p>My first instinct was to \u201cfix everything\u201d:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>turn the first cell into a caption<\/li>\n\n\n\n<li>restructure the entire table<\/li>\n<\/ul>\n\n\n\n<p>But I had to pause.<\/p>\n\n\n\n<p>Just because something <em>looks<\/em> like a caption doesn\u2019t mean it is one.<\/p>\n\n\n\n<p>So instead, I made the smallest safe improvements:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>convert the first row into <code>&lt;thead><\/code><\/li>\n\n\n\n<li>convert its cells into <code>&lt;th><\/code><\/li>\n\n\n\n<li>leave everything else as-is unless I was sure<\/li>\n<\/ul>\n\n\n\n<p>The goal wasn\u2019t perfection\u2014it was <strong>correctness without guessing<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lesson #3: Real-world HTML is messy<\/h2>\n\n\n\n<p>I expected all tables to look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;table&gt;\n  &lt;tr&gt;...&lt;\/tr&gt;\n&lt;\/table&gt;\n<\/code><\/pre>\n\n\n\n<p>But some looked like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;table&gt;\n  &lt;tbody&gt;\n    &lt;tr&gt;...&lt;\/tr&gt;\n  &lt;\/tbody&gt;\n&lt;\/table&gt;\n<\/code><\/pre>\n\n\n\n<p>My code initially missed those rows entirely.<\/p>\n\n\n\n<p>Which led to a bug where I returned <code>null<\/code>\u2026<br>and accidentally removed the entire table.<\/p>\n\n\n\n<p>That one line broke everything.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lesson #4: Small implementation details matter more than you think<\/h2>\n\n\n\n<p>In the parser I was using:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>undefined<\/code> \u2192 keep the node<\/li>\n\n\n\n<li><code>null<\/code> \u2192 remove the node<\/li>\n<\/ul>\n\n\n\n<p>That difference isn\u2019t obvious until it is.<\/p>\n\n\n\n<p>It\u2019s the kind of thing that doesn\u2019t show up in the UI right away\u2014but can quietly break content.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lesson #5: Performance fixes can cancel themselves out<\/h2>\n\n\n\n<p>I optimized the parsing with <code>useMemo<\/code> so it wouldn\u2019t run on every render.<\/p>\n\n\n\n<p>Then I used random keys.<\/p>\n\n\n\n<p>Which forced React to re-mount everything anyway.<\/p>\n\n\n\n<p>Same result. Just more complicated.<\/p>\n\n\n\n<p>The fix was simple:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>use stable keys tied to the data<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lesson #6: Defensive code is not overengineering<\/h2>\n\n\n\n<p>I added a filter to only process <code>&lt;td&gt;<\/code> and <code>&lt;th&gt;<\/code> elements.<\/p>\n\n\n\n<p>At first glance, it looked unnecessary.<\/p>\n\n\n\n<p>But parsed HTML often includes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>whitespace nodes<\/li>\n\n\n\n<li>unexpected elements<\/li>\n<\/ul>\n\n\n\n<p>Without that filter, I\u2019d end up processing things that weren\u2019t actually cells.<\/p>\n\n\n\n<p>When your input isn\u2019t fully controlled, defensive code isn\u2019t optional.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What I actually changed<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Introduced <code>&lt;thead><\/code> and <code>&lt;tbody><\/code> for structure<\/li>\n\n\n\n<li>Converted header cells to <code>&lt;th><\/code><\/li>\n\n\n\n<li>Preserved existing content without over-assuming intent<\/li>\n\n\n\n<li>Memoized parsing to avoid unnecessary work<\/li>\n\n\n\n<li>Replaced unstable keys with deterministic ones<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The real takeaway<\/h2>\n\n\n\n<p>This wasn\u2019t about tables.<\/p>\n\n\n\n<p>It was about alignment.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Does your code structure reflect the meaning of what you\u2019re building?<\/p>\n<\/blockquote>\n\n\n\n<p>Because when it doesn\u2019t, the UI might still look fine\u2014but the experience breaks for the people who rely on that structure the most.<\/p>\n\n\n\n<p>And those issues are easy to miss if you\u2019re only looking at the screen.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I thought I was fixing a small accessibility issue. We had tables rendering with only&hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[],"class_list":["post-41","post","type-post","status-publish","format-standard","hentry","category-engineering-lab"],"_links":{"self":[{"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/posts\/41","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=41"}],"version-history":[{"count":1,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/posts\/41\/revisions"}],"predecessor-version":[{"id":42,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/posts\/41\/revisions\/42"}],"wp:attachment":[{"href":"https:\/\/vanianettleford.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=41"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=41"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=41"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}