How to colour form controls in data tables with sticky headers

I prototyped form controls in data tables. Each row contains input and select controls colored by their type. Labels stay in view via sticky column headers.

In this post, I reflect on how I prototyped form controls in data tables. Each body row contains input and select controls, coloured by the selected type. Form controls are labelled by text in the column header. Important for sighted users, I prevent header labels from scrolling out of view by styling them as position: sticky. Finally, I made the table responsive so users can edit when using a narrower screen.

See the Pen data-type coloured input and select elements in a sticky header table by Vernon Fowler (@vfowler) on CodePen.

See my Pen data-type coloured input and select elements in a sticky header table on CodePen.


The task in this interface requires editing form controls across multiple rows in a bulk update. Users perform this task mainly on desktop-class devices.

I baked in as much inclusive design as I could, drawing upon works from Heydon Pickering, Andrew Coyle, Chris Coyier, Eric Bailey, Adam Silver, and Scott O’Hara. This was my first time to write JavaScript for a basic interaction. I also learned several CSS tricks and practised a little ARIA. The prototype mostly supports Internet Explorer 10, a browser some users are stuck with. The resulting data table with form controls should operate well via mouse, keyboard and touch interaction, for sighted people as well as those using a screen reader.

Case study background

This widget designer is an internal-facing part of data visualisation software. It is where visualisations are crafted before they appear on a dashboard. When a new data visualisation widget is needed, the complete interface is used to:

  1. write a data query;
  2. create views with appropriate types of visualisation;
  3. flesh out a title and description; and
  4. configure widget parameters

This prototype focuses on the last step, configuring parameters.

A user story

As a widget designer, 
I want to configure parameters and validation rules
so that dashboard users can safely set custom data filters and visualisation threshold values.

Keywords from the data query can be parameterised. A widget designer does this so that dashboard users can then set filters and threshold values. For instance, to enable start and end date filters, parameters must be set for them.

Parameters enable dashboard users to set custom filters such as start and end dates.

Rules can be configured between any pair of parameters. For example, the start date must occur before the end date. A violation of rules should induce an invalid state and display an error message. Rules can only be configured between parameters of the same type. To help make this association, I colour coded form controls for each parameter type.

Parameter typeColour nameHEX
Parameter types and their associated colours

Colour contrast ratios and simulated vision tests

My table rows are styled with alternating background colours, also known as zebra striping. Odd rows are almost white. I applied a pale grey (#E7ECF5) to every second row, to reduce the overall intensity of screen brightness. Next, I checked the contrast of each parameter type colour against the table row backgrounds.

A grid containing results of testing contrast ratios of 6 colours against zebra striped table row backgrounds
In this Contrast Grid I ordered colours from least to most contrast.

I tested the colour combination with the least contrast, BlueBerry text on the pale grey background. With non-bold text, this colour combination passes WCAG 1.4.3 Contrast (Minimum) and all simulated tests, except direct sunlight. Other colour combinations in the grid have higher contrast ratios, and more than half of them meet the AAA standard for Contrast (Enhanced).

To arrive at these final colours, I adjusted the lightness of nearby colours until the text and background colour combinations passed simulated tests for cataracts and low vision. According to, cataracts and low vision affect 33% and 31% of the population. It’s definitely worth designing for up to one in three users. A contrast ratio of 5.5:1 satisfies these simulated tests.

The whocanuse tool indicates whether a colour combination is okay for various colour vision deficiencies and vision types.
The whocanuse tool has controls for adjusting hue, saturation and lightness. This makes it easy way to nudge a colour until it passes simulated tests.

All the colours I selected have excellent contrast ratios and pass simulated tests for low vision, glaucoma, cataracts, colour vision deficiencies and night shift mode situations.

Styling form controls in data tables with colour

Sticky headers with coloured form controls in data tables body rows
Table rows with data-type=date attribute used to style form controls with brown color.

To colour the form elements in each row, I first manually added data-type attributes to each HTML table row.

<tr data-type="Date">

Then in CSS I use case-insensitive attribute selectors to style the form controls with colour corresponding to the data-type. For instance:

[data-type="date" i] input,
[data-type="date" i] select {
  color: #8A4500;
  border-color: #8A4500;

As a static mockup, this works well. Ten table rows were enough to show all the variations of parameter types with form controls in their associated colours. However, to make an interactive prototype, I needed to dynamically change the colour whenever anyone selects a different parameter type.

Select a type and JavaScript changes the colour

Example form controls in data tables rows with colours determined by their data-type
When a widget designer selects a type, the form controls change colour.

In this iteration, I needed the data-type attribute to be set dynamically. Setting data-type="Date" or data-type="Number" was a job for JavaScript. Each table row needs a value set for the data-type attribute:

  1. initially, after the page has completed loading via document.readyState === "complete"
  2. after anyone changes a <select> value via addEventListener("change" …

Check out the JavaScript functions I coded in my Codepen prototype. I coded my JavaScript such that in the HTML, each <select> must:

Form controls in data tables need unique id attributes. Display form details via Chris Pederick's Web Developer browser extension.
I’ve scoped the script to only apply to each <select> with [id] and [aria-labelledby] attributes, and nested in a <tr>.

Success! For me, JavaScript skill development has been long deferred. I’m chuffed. My new skills were also needed in my very next prototype!

An inclusive data table component

My table is based on Heydon Pickering’s data tables component. I adopted his technique of using a heading inside the table caption.

  <h3>Parameters configuration</h3>

This gives assistive technology users three ways to discover the table: by table shortcut, heading shortcut, or just by browsing downwards.

The table also has scope attributes on both column and row headers, adding extra clarity and context. For example, HTML such as <th scope="col">Column header 1</th> and <th scope="row">Row header 1</th>.

Some screen readers will announce both the column and row labels for each of the data cells.

Heydon Pickering, Data Tables

Not only do these attributes enrich the table with semantics, but they also provide us attribute selectors to style with CSS. This saves us from needing to come up with CSS class names. It also makes style sheets easier to understand and maintain. It’s a win-win situation.

Labelling form controls in data tables

Every form control should always use a label. Using <label> elements for <input> and <select> in every row would creates repetition. Before, labelling form controls in data tables with a title attribute was recommended. In 2020, title has been superseded by either an aria-label or aria-labelledby attribute. But controls labelled with aria-label attribute will be ignored by automatic translators. Table column headers contain texts for labels. So I gave each form control an aria-labelledby attribute. This references text in the relevant column header.

<th><span id="type-header">Type</span>...</th>
<select id="row1select" aria-labelledby="type-header">

Screen reader users benefit from programmatic association between form controls and label text.

Compensating for a lack of clickable labels

A downside of using anything other than a <label> element, is the lack of a clickable label. You may already know clicking a label moves focus to its associated form field. Also, a label element provides a larger hit area. As a result, motor-impaired users can easily move focus to the field. Hence why Adam Silver states a clickable label is essential to the experience.

To compensate for this, I made the form controls taller and their font-size larger.

Sticky table headers

When a table has many rows, users need to scroll down to see more data in their available screen height. Without sticky headers, they lose sight of headers in the top row. Now with our supersized form controls, this loss of meaning occurs in tables with anything more than a few rows.

The CSS Trick for making sticky headers inside a <table> is to apply position: sticky; to the <th> elements. In our case, we do not want a sticky left column – which have <th scope="row">. So our CSS needs a more specific selector than just th.

th[scope="col"] {
    position: sticky;
    top: 2.25rem;
    z-index: 150;

Sticky table headers are superb when people need to scroll down to see more rows. Sticky headers are the only aspect of my prototype not natively supported in IE browsers. It’s possible to find a polyfill via resources at Can I use CSS position:sticky. If any of our internal widget designers are stuck on IE I would explore a suitable option.

A responsive table for narrow view

Heydon’s data tables component goes responsive by letting the table’s parent element scroll horizontally. Because this table has action links in the right column, I chose a different approach. The interaction cost of repeatedly scrolling horizontally to reach an action link is too high.

I adopted a responsive data table solution to accommodate narrower screens. At narrow widths, the sticky headers are redundant. This narrow two-column layout presents headers on the left, and the data or form controls on the right.

Testing a <select> form control for my responsive data table in an iPhone

Although a smartphone would be a challenging device for this task, it is good practice to test a design for extremes.

Focus on the micro-task

In the screenshot above, note the highlighted background colour on the row containing the focused form control. When Eric Bailey presented If it’s interactive, it needs a focus style, he mentioned the focus style technique for rows in data tables. To only highlight the body rows, I use tbody tr:focus-within as my CSS selector. A highlighted row can help sighted users refocus after distractions, and more easily resume their task.

Action links making sense

In the last column, I have dummy action links. Sighted users will be able to see 3 different link phrases in the Actions column.

  • Add rules
  • Edit rules
  • Edit options

Sighted keyboard users can see row highlighting when focus is on the link. Zebra striping helps mouse users identify which row the link relates to. However, hearing a screen reader announce these as a list of links needs more context. 

So to add context to each link, I appended a descriptor for screen reader users only. I use Scott O’Hara’s visually hidden technique as it’s supported in browsers such as IE9. For example, <a href="#">Edit rules <span class="sr-only">for Start_date</span></a>. As a result, screen reader users will be able to make sense of action links, even when presented out of context.

Summing up

Form controls in data tables presented new challenges for me. I am glad I finally got to learn and apply JavaScript. Combining these with my prior learning on web forms, a dash of ARIA, and some inclusive design has been a total blast! Above all, the thought I put into this prototype represents some of my best work on user-friendly interfaces.

With more time I would:

  • Learn how to make the sort functionality work
  • Find a suitable polyfill to make sticky headers work in IE browsers
  • Conduct testing of the prototype with real users and more devices