Reducing Nodes per Cell

Permalink to "Reducing Nodes per Cell"

A 1,000-row table with 10 columns has 10,000 cells. If each cell contains six nested elements — a wrapper, a flex container, an icon, a span for text, a tooltip trigger, a hidden tooltip — that is 60,000 elements before the table chrome. Screen readers build an accessibility tree from all of it and slow down noticeably: NVDA and JAWS can take seconds to load such pages and lag on every key press. Reducing nodes per cell is often the cheapest accessibility performance win.

This guide belongs to DOM size limits and performance trade-offs.

Spec reference

Permalink to "Spec reference"
  • SC 4.1.2 (A) — semantics must remain correct after simplification.
  • SC 1.3.1 (A) — header relationships unchanged.
  • Accessibility tree — each element can create a node; generic wrappers are often pruned but still cost processing.
  • Lighthouse DOM size audit — flags pages with more than about 800 body elements and treats more than 1,400 as excessive.
Elements per cell before and after Bar chart of elements per cell in a component library table: before simplification 7; after removing wrappers 4; after CSS decoration 3; after sharing tooltips 1. Elements per cell before and afterBeforeelements per cellWrappers removedelements per cellCSS decorationelements per cellShared tooltipselement per cell
Most cells need only the cell element and text.

When to use — and when not to

Permalink to "When to use — and when not to"

Apply this to any table or grid with more than a few hundred rows rendered, or when screen reader users report lag. Combine with pagination or virtualization for very large data.

Do not remove elements that carry semantics: headers, buttons, links, inputs and their labels. The misapplication: replacing buttons with styled spans to save nodes, breaking keyboard and screen reader access.

Heavy cells versus lean cells Comparison: heavy cells contain wrappers, icon elements and hidden tooltips; lean cells contain text plus only interactive elements needed, with decoration in CSS and shared popovers. Heavy cells versus lean cells✗ div > div > span > textIcon element per statusHidden tooltip per cellMenu markup per row✓ td > textCSS ::before with text labelOne shared tooltipOne shared menu

Annotated code example

Permalink to "Annotated code example"
<!-- Before: 7 elements -->
<td><div class="cell"><div class="cell-inner"><span class="icon"><svg viewBox="0 0 20 20">…</svg></span>
  <span class="text">Overdue</span><div class="tooltip" hidden>Due 2 March</div></div></div></td>

<!-- After: 1 element, decoration in CSS, tooltip shared -->
<td class="status status-overdue" data-tip="Due 2 March">Overdue</td>
td.status::before { content: ""; display: inline-block; inline-size: .6em; block-size: .6em;
  border-radius: 50%; margin-inline-end: .4em; background: currentColor; }      /* decorative dot */
td.status-overdue { color: var(--status-danger); }
// One tooltip for the whole table
const tip = document.getElementById('table-tip');
table.addEventListener('focusin', e => showTip(e.target.closest('[data-tip]')));
table.addEventListener('mouseover', e => showTip(e.target.closest('[data-tip]')));

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Change Screen reader effect
Remove wrappers No change to announcements; faster navigation
Status icon to CSS with text “Overdue” still read; icon not announced
Shared tooltip Description via aria-describedby on focused element only
Shared row menu Menu button per row remains; menu content shared
Fewer nodes overall Faster page load in browse mode
What to keep and what to remove Matrix of cell contents — layout wrappers, decorative icons, text spans, interactive controls, per-cell tooltips — against keep, replace with CSS, share or remove. What to keep and what to removeKeepReplace with CSSShareRemoveLayout wrappersNoGrid/flex on tdNoYesDecorative iconsNoYesNoIf redundantText spansIf styledOftenNoOftenInteractive controlsYesNoNoNoPer-cell tooltipsNoNoYesNo
Keep semantics; move decoration to CSS; share overlays.

Step-by-step implementation

Permalink to "Step-by-step implementation"
  1. Measure. Count elements per cell and total nodes (document.querySelectorAll('*').length) and record screen reader load time.
  2. Flatten. Remove wrappers created by component abstractions; apply layout to the cell itself.
  3. Move decoration to CSS. Pseudo-elements and backgrounds for dots, bars and icons that duplicate text.
  4. Share overlays. One tooltip element, one row menu and one popover per table, positioned on demand.
  5. Re-measure. Compare node counts and screen reader responsiveness before and after.

Integration context

Permalink to "Integration context"

Measuring tree cost is covered in measuring accessibility tree cost, and budgets in setting a DOM node budget in CI. When reduction is not enough, see pagination versus virtualization for large tables and two-dimensional virtualized grids. Component library defaults are compared in React data grid libraries.

Gotchas

Permalink to "Gotchas"

CSS content is sometimes read. Text in ::before content can be announced; use empty content for decorative shapes, or alternative text syntax where supported.

Icon-only meaning. If an icon conveys meaning without text, it needs a text equivalent; do not remove it without adding text.

Framework keys. Flattening components can change keys and remount rows; test focus retention.

Edge cases

Permalink to "Edge cases"
  • Rich cells. Some cells genuinely need structure (avatar plus name plus role); keep them, but simplify neighbours.
  • Editable cells. Render editors only when editing, not hidden in every cell.
  • Third-party grids. Check configuration options for “lightweight cells” or custom renderers.

Worked example

Permalink to "Worked example"

An HR system’s employee table rendered 800 rows with nine elements per cell from a design system’s cell component. JAWS took eight seconds to load the page and lagged on every arrow key. The team flattened the cell component, moved status dots to CSS, shared one tooltip and one row menu, and rendered editors on demand. Elements dropped from 72,000 to 9,000, JAWS loaded in under two seconds and navigation lag disappeared.

Common mistakes

Permalink to "Common mistakes"
  • Removing semantic elements.
  • Decorative CSS content that is read aloud.
  • Hidden editors in every cell.
  • Not measuring before and after.

Testing checklist

Permalink to "Testing checklist"

Testing it with a screen reader

Permalink to "Testing it with a screen reader"

Load the page with NVDA and time how long until “document” is announced and browse mode is ready; repeat after changes. Navigate cells with table commands and listen for any newly announced decorative content, such as pseudo-element text. Confirm tooltips are read as descriptions when focus lands on cells with them. Repeat in JAWS, which is often more sensitive to DOM size.

Talking to design system teams

Permalink to "Talking to design system teams"

Node bloat usually comes from shared components, so fixes belong there. Bring numbers: elements per cell, total nodes on a typical page and screen reader load time. Propose a lean cell variant for data-heavy tables and a shared overlay service. Design system teams are more receptive to measured, scoped changes than to a request to rewrite components, and the improvement then reaches every product using them.

Auditing a table quickly

Permalink to "Auditing a table quickly"

To find where nodes come from, run a short script in the console: pick one row, count row.querySelectorAll('*').length, and list the tag and class of each descendant. Patterns jump out quickly — a wrapper per cell from a layout component, an SVG per status icon, hidden tooltip markup. Compare with the accessibility tree in browser developer tools to see which of those elements actually produce nodes. Fix the largest contributors first; removing one wrapper from every cell often saves more than any clever optimisation elsewhere.

Key points

Permalink to "Key points"

Measure first, flatten wrappers, move decoration into CSS, share overlays across the table, keep every semantic element and measure again to confirm screen readers respond faster.

Design system notes

Permalink to "Design system notes"

Provide a lean cell renderer and shared overlay services, and set a per-cell element budget in component guidelines.

FAQ

Permalink to "FAQ"
Do generic divs affect screen readers?

They are often pruned from the accessibility tree but still cost DOM and layout work.

How many nodes per cell is reasonable?

One to three for most cells.

Will CSS pseudo-elements be read?

Text content may be; use empty content for decoration.

Should I virtualize instead?

Often both; lean cells make virtualization smoother too.

Do shared tooltips work with screen readers?

Yes, with aria-describedby set on the focused element.

How do I measure accessibility tree size?

See the measuring guide; browser devtools and scripts can count nodes.

Permalink to "Related"

← Back to DOM Size Limits and Accessible Performance Tradeoffs