2

Take the two headers table example from the w3c wcag tutorials

table {
  border-collapse: collapse;
  border-spacing: 0
}

table th {
  text-align: left;
  background-color: #ccc
}

table th,
table td {
  padding: .5em;
  border: 1px solid #999
}
<table>
  <tr>
    <td></td>
    <th>Monday</th>
    <th>Tuesday</th>
    <th>Wednesday</th>
    <th>Thursday</th>
    <th>Friday</th>
  </tr>
  <tr>
    <th>09:00 - 11:00</th>
    <td>Closed</td>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>11:00 - 13:00</th>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>13:00 - 15:00</th>
    <td>Open</td>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>15:00 - 17:00</th>
    <td>Closed</td>
    <td>Closed</td>
    <td>Closed</td>
    <td>Open</td>
    <td>Open</td>
  </tr>
</table>

Can this be styled without changing the markup (it must stay accessible) so that on a responsive mobile view it collapses into vertical table something like this:

Monday

09:00 - 11:00 Closed

11:00 - 13:00 Open

13:00 - 15:00 Open

15:00 - 17:00 Closed

Tuesday

09:00 - 11:00 Open

11:00 - 13:00 Open

13:00 - 15:00 Open

15:00 - 17:00 Closed

etc

Penny Liu
  • 15,447
  • 5
  • 79
  • 98
Jonny
  • 796
  • 2
  • 10
  • 23
  • yes : https://codepen.io/gc-nomade/pen/VwwweeX , i'll be looking for the question it went with – G-Cyrillus Mar 21 '20 at 11:02
  • @G-Cyrillus I really like that example. It is pretty much exactly what I am looking for. The only question is I think it requires duplicating the data in an attribute (date-time in the codepen example). It there was a way without this - that would be awesome - otherwise I guess I might be looking at changing the markup slightly (or using some javascript to insert the attributes) – Jonny Mar 21 '20 at 11:09
  • for the data-attribute, if your table is generated from the server side, include it in the template else on the browser side, javascript could be used too to setattribute and value needed ;) – G-Cyrillus Mar 21 '20 at 11:11
  • You shouldn't close this as it is marked as 'accessibility', there are numerous problems with the way `display: contents` has been implemented that mean it is a problem for accessibility. See https://hiddedevries.nl/en/blog/2018-04-21-more-accessible-markup-with-display-contents under the section 'Accessibility concerns with current browser implementations of display: contents' – GrahamTheDev Mar 21 '20 at 11:12
  • @GrahamRitchie - Good point (ps - i didn't close it). Would accessibility be covered because the markup is still semantic and the display: contents is only being as duplicate information? I.e. the y-axis headers are still in cells. It would still have been cool to have a css only answer - but I understand why it might not be possible. – Jonny Mar 21 '20 at 11:25
  • here is a version with javascript filling up the data-attribute for you https://codepen.io/gc-nomade/pen/abbbddW – G-Cyrillus Mar 21 '20 at 11:27
  • I actually am not sure is a CSS only answer is possible but will have a fiddle when @G-Cyrillus sees my comment and reopens (they will I have no doubt). I do tend to use a tiny bit of JS when I do this but I am now intrigued if I can come up with a CSS only solution for you – GrahamTheDev Mar 21 '20 at 11:27
  • @GrahamRitchie display:contents here should not be an issue, it is already a table and it will dealed as a table from screen readers, data will not be mixed up, the html remains relevant, it is only reordered at screen inserting the pseudo . for the pseudo repeatingtoo many times the same information , they can be shut off : https://stackoverflow.com/questions/26634156/can-i-prevent-after-pseudo-element-from-being-read-by-screen-readers ;) – G-Cyrillus Mar 21 '20 at 11:31
  • I ran it on a screen reader, it doesn't even register as a table once `display:contents` is active, it fundamentally changes the way it is presented to screen readers. Fire up NVDA and try your example, you cannot even find the table using 'T', never mind trying to navigate via cells with 'Alt+Ctrl+arrows'. It is a bug with the implementation. – GrahamTheDev Mar 21 '20 at 11:33
  • If you don't have a screen reader just open up dev tools and inspect the table headers in desktop mode, then reinspect in mobile mode, you will see it changes from 'table header' to 'generic' which removes all semantic meaning. – GrahamTheDev Mar 21 '20 at 11:36
  • okay, then ,screen readers can also be filtered via mediaquery . What would you recommend ? retrieve all the data from the table and build a new one from scratch ? – G-Cyrillus Mar 21 '20 at 11:38
  • 1
    https://www.powermapper.com/tests/screen-readers/content/media-query-speech/ and https://www.powermapper.com/tests/screen-readers/content/media-query-aural/, those media queries fail miserably in screen readers. There is nothing wrong with your CSS, it is a problem with browsers implementation. – GrahamTheDev Mar 21 '20 at 11:43
  • 1
    My recommendation would be some different implementation, I do not have a CSS only solution to hand, I always use a little JavaScript to change items into `
    `s and then use `WAI-ARIA` properties to expose semantic meaning to screen readers. It could be that using your method with that is far better but would still require JS at that point. It is likely impossible with just CSS but I feel that needs exploring in the answers section (someone better with CSS like yourself after knowing the issue may come up with an alternative).
    – GrahamTheDev Mar 21 '20 at 11:46
  • 1
    okay, let's see who comes for te accessibility part, its on my favorite now – G-Cyrillus Mar 21 '20 at 11:47
  • This is one of the best comments discussions I have witnessed. If I have read it correctly best position so far is: Add data attributes and style accordingly, use a media query to filter out screen readers - however implementation of media query is poor so fails to address the point. – Jonny Mar 21 '20 at 11:57
  • Basically @G-Cyrillus has offered a great solution, sadly all webkit browsers have a bug that means that although his solution is correct (CSS should NOT interfere with semantics, but in this case it does), we have to work around a problem that web browsers have introduced. My solution uses JavaScript so I am hoping I can call on G-Cyrillus's example as inspiration of a compromise that works CSS only, if not I will use what he has done to add the relevant WAI-ARIA for screen readers. Between us we should give you a great answer. – GrahamTheDev Mar 21 '20 at 12:00
  • Going out now but will have a play this evening for you and hopefully I will get something good for you. Thanks @G-Cyrillus for reopening. – GrahamTheDev Mar 21 '20 at 12:02
  • would upddating the role attribute be helpfull ? test example : https://codepen.io/gc-nomade/pen/gOpjVJW I have no screen readers and no one around to test this for me :( – G-Cyrillus Mar 21 '20 at 12:09

2 Answers2

2

Not a finished answer as I will make fiddles for each scenario, just wanted to get the thought process down for you to play with - UPDATE: 3 / 4 fiddles done

I have played with a lot of options here, fun challenge!

Short Answer

The short answer to your question is no, you cannot keep the structure given, make it responsive and keep it accessible using only CSS.

I would be so bold as to say it is impossible to do what you ask, as it would involve using some sort of display properties that would break the accessibility tree (i.e. if we used display: block that immediately breaks the accessibility tree).

I even considered using position: absolute (a terrible idea but wanted to see if it could be done) but that also immediately breaks the accessibility tree.

Due to the structure of the data the only way I could think to do this with CSS only would involve hundreds of :before, :after and content: CSS properties / selectors that would just be a mess. Therefore i didn't explore that option but it could be possible that way for someone determined enough to make this work.

Why would this not be possible?

I think the answer heralds back to the good ol' days when web designers used tables for layout. The second any positioning is applied the browser assumes that you are using a table for layout and so removes it from the accessibility tree (as it would make a website horrendous to use if left in the accessibility tree and a table was used for layout!)

What are the options then?

I have a few different ways you could achieve the end result. I will attempt to weigh the pros and cons for each, but every single one of them breaks at least one of your requirements.

Option 1. Create a second table for mobile view and switch them out using media queries.

By creating a second table you can present the information differently for mobile vs tablet and desktop.

I am quite surprised at how many positives I found for this method.

  • It allows you to maintain your original markup (albeit you have to add markup for the second table)
  • Switching out the tables is literally one CSS selector (display: none or display: table) within a media query.
  • Accessibility would be maintained (assuming you build the second table correctly)
  • Performance - I actually thought adding a second table would be bad for load times but after some though I realised that it will likely make very little difference as the reduced CSS requirement would counter the increased HTML, plus tables compress well with GZIP etc.
  • Doesn't require any JavaScript (but it would require creating the table twice either programatically or manually in the backend)

Option 1 example

(view full screen for desktop / original view)

Thinking I had as I built this example - I am actually unsure whether the following table is semantically correct. It is a bit unusual adding <th> in any place other than headers and footers / first columns in a table. It passes validation and seems ok on a screen reader but still you may want to check it yourself.

table {
  border-collapse: collapse;
  border-spacing: 0
}

table th {
  text-align: left;
  background-color: #ccc
}

table th,
table td {
  padding: .5em;
  border: 1px solid #999
}


/*start with mobile view hidden*/
table:nth-child(2){
  display:none;
}

@media only screen and (max-width: 720px) {
/*toggle second table in for mobile view*/
    table:nth-child(1){
  display:none;
}
table:nth-child(2){
  display:table;
}
}
<table>
  <tr>
    <td></td>
    <th>Monday</th>
    <th>Tuesday</th>
    <th>Wednesday</th>
    <th>Thursday</th>
    <th>Friday</th>
  </tr>
  <tr>
    <th>09:00 - 11:00</th>
    <td>Closed</td>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>11:00 - 13:00</th>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>13:00 - 15:00</th>
    <td>Open</td>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>15:00 - 17:00</th>
    <td>Closed</td>
    <td>Closed</td>
    <td>Closed</td>
    <td>Open</td>
    <td>Open</td>
  </tr>
</table>

<table>
<tr>
  <th colspan="2">Monday</th>
</tr>
<tr>
  <td>09:00 - 11:00</td>
  <td>Closed</td>
</tr>
<tr>
  <td>11:00 - 13:00</td>
  <td>Open</td>
</tr>
<tr>
  <td>13:00 - 15:00</td>
  <td>Open</td>
</tr>
<tr>
  <td>15:00 - 17:00</td>
  <td>Closed</td>
</tr>
<tr>
  <th colspan="2">Tuesday</th>
</tr>
<tr>
  <td>09:00 - 11:00</td>
  <td>Open</td>
</tr>
<tr>
  <td>11:00 - 13:00</td>
  <td>Open</td>
</tr>
<tr>
  <td>13:00 - 15:00</td>
  <td>Open</td>
</tr>
<tr>
  <td>15:00 - 17:00</td>
  <td>Closed</td>
</tr>
<tr>
  <th colspan="2">Wednesday</th>
</tr>
<tr>
  <td>09:00 - 11:00</td>
  <td>Open</td>
</tr>
<tr>
  <td>11:00 - 13:00</td>
  <td>Closed</td>
</tr>
<tr>
  <td>13:00 - 15:00</td>
  <td>Open</td>
</tr>
<tr>
  <td>15:00 - 17:00</td>
  <td>Closed</td>
</tr>
<tr>
  <th colspan="2">Thursday</th>
</tr>
<tr>
  <td>09:00 - 11:00</td>
  <td>Closed</td>
</tr>
<tr>
  <td>11:00 - 13:00</td>
  <td>Closed</td>
</tr>
<tr>
  <td>13:00 - 15:00</td>
  <td>Closed</td>
</tr>
<tr>
  <td>15:00 - 17:00</td>
  <td>Open</td>
</tr>
<tr>
  <th colspan="2">Friday</th>
</tr>
<tr>
  <td>09:00 - 11:00</td>
  <td>Closed</td>
</tr>
<tr>
  <td>11:00 - 13:00</td>
  <td>Closed</td>
</tr>
<tr>
  <td>13:00 - 15:00</td>
  <td>Closed</td>
</tr>
<tr>
  <td>15:00 - 17:00</td>
  <td>Open</td>
</tr>
</table>

Option 2. Just make the table scrollable on mobile.

Another one where on paper it looks like a good option but I just don't like it. Having a table that scrolls is one of the worst mobile experiences I can think of (exception being massive data sets).

It is also a questionable accessibility point, screen readers would be fine but is a scrollable table accessible for those with accuracy issues? Probably not.

Option 2 Example

Just make the screen smaller than 720px to see the scroll bars appear for horizontal scrolling.

table {
  border-collapse: collapse;
  border-spacing: 0;
  min-width: 720px;
}

table th {
  text-align: left;
  background-color: #ccc
}

table th,
table td {
  padding: .5em;
  border: 1px solid #999
}

DIV{
  overflow-x:auto;
}
<div>
<table>
  <tr>
    <td></td>
    <th>Monday</th>
    <th>Tuesday</th>
    <th>Wednesday</th>
    <th>Thursday</th>
    <th>Friday</th>
  </tr>
  <tr>
    <th>09:00 - 11:00</th>
    <td>Closed</td>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>11:00 - 13:00</th>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>13:00 - 15:00</th>
    <td>Open</td>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th>15:00 - 17:00</th>
    <td>Closed</td>
    <td>Closed</td>
    <td>Closed</td>
    <td>Open</td>
    <td>Open</td>
  </tr>
</table>
</div>

Option 3. Don't use a table at all and instead use different markup and make it look like a table.

This may seem like a stupid idea, and in a lot of ways it is! The CSS would be hard work and it isn't semantically correct.

Although the markup would end up being semantically correct (from a document structure perspective) as you have a "heading" (<h2>Monday</h2>) and then a "sub-heading" (<h3>09:00 - 11:00</h3>) followed by relevant information <p>Closed</p>. It just means the visual styling is not quite the same as the HTML information.

The only advantage to this method is we don't have to rely on WAI-ARIA in order to make it work and we can position things however we want visually without impacting screen readers. WAI-ARIA isn't 100% perfect support, so this could be the most accessible solution of them all. Down side is it is a complete change of data from what you started with.

Option 3 Example

Please do a better CSS job of this than me, but the principle is sound.

Notice how I have the aria-hidden section at the beginning for the 'desktop' view (to hide it from screen readers but not visually) and how I use the visually hidden technique on the .col h3 selector. This way as far as a screen reader is concerned the HTML is exactly the same for both.

.container{
  width: 60%;
}


@media only screen and (min-width: 1024px) {
h2, .desktop h3, p{
  outline: 1px solid #333;
  outline-offset: -1px;
  padding: 0px 10px;
  margin: 0;
}


div{
  float: left;
  width: 15%;
  line-height: 2rem;
}
h2{
  line-height: 3rem;

}
.col h3{
  position: absolute !important;
    height: 1px; 
    width: 1px;
    overflow: hidden;
    clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
    clip: rect(1px, 1px, 1px, 1px);
    white-space: nowrap; /* added line */
}
.desktop{
  width: 25%;
  margin-top: 3rem;
  float:left;
  display: block;
}
.desktop h3{
  display: block;
  line-height: 2rem;
}

}

@media only screen and (max-width: 1023px) {
  .desktop{
    display: none;
  }
  h2{
    width: 100%;
    display: block;
    float: left;
    padding: 10px 0px 10px 0px;
    margin: 5px 0;
    border-top: 1px solid #666;
    border-bottom: 1px solid #666;
  }
  h3{
    width: 50%;
    display: block;
    float:left;
    line-height: 2rem;
     padding: 0px;
    margin: 0;
  }
  .col p{
    width: 50%;
    display: block;
    float:left;
    line-height: 2rem;
     padding: 0px;
    margin: 0;
  }
  
  
  
}
<div class=container>
<div class="desktop" aria-hidden="true">
  <h3>09:00 - 11:00</h3>
  <h3>11:00 - 13:00</h3>
  <h3>13:00 - 15:00</h3>
  <h3>15:00 - 17:00</h3>
</div>
<div class="col">
<h2>Monday</h2>
<h3>09:00 - 11:00</h3>
<p>Closed</p>
<h3>11:00 - 13:00</h3>
<p>Open</p>
<h3>13:00 - 15:00</h3>
<p>Open</p>
<h3>15:00 - 17:00</h3>
<p>Closed</p>
</div>
<div class="col">
<h2>Tuesday</h2>
<h3>09:00 - 11:00</h3>
<p>Open</p>
<h3>11:00 - 13:00</h3>
<p>Open</p>
<h3>13:00 - 15:00</h3>
<p>Open</p>
<h3>15:00 - 17:00</h3>
<p>Closed</p>
</div>
<div class="col">
<h2>Wednesday</h2>
<h3>09:00 - 11:00</h3>
<p>Open</p>
<h3>11:00 - 13:00</h3>
<p>Closed</p>
<h3>13:00 - 15:00</h3>
<p>Open</p>
<h3>15:00 - 17:00</h3>
<p>Closed</p>
</div>
<div class="col">
<h2>Thursday</h2>
<h3>09:00 - 11:00</h3>
<p>Closed</p>
<h3>11:00 - 13:00</h3>
<p>Closed</p>
<h3>13:00 - 15:00</h3>
<p>Closed</p>
<h3>15:00 - 17:00</h3>
<p>Open</p>
</div>
<div class="col">
<h2>Friday</h2>
<h3>09:00 - 11:00</h3>
<p>Closed</p>
<h3>11:00 - 13:00</h3>
<p>Closed</p>
<h3>13:00 - 15:00</h3>
<p>Closed</p>
<h3>15:00 - 17:00</h3>
<p>Open</p>
</div>
</div>

Option 4. Use another method (i.e. the suggestion by G-Cyrillus) and use WAI-ARIA to maintain the accessibility.

I really liked how clean the method that user G-Cyrillus suggested is, in terms of separation of concerns. CSS for visuals, HTML for semantics, it is what I aim for every day.

If it wasn't for this browser bug (which now that I think about it may not be a bug due to what I said earlier about why you can't use display properties on tables) this solution would be perfect (well also 90% coverage on display:contents is a little low for my liking, but that is due to the space I operate in and I am sure there would be a suitable poly-fill for the rubbish browsers!)

For this method the solution would be to utilise WAI-ARIA.

This allows for attributes to be added to elements to add further information or describe the semantics of an element to a screen reader. This would help us replace the semantics information that gets destroyed once you apply position or display styling to a table.

For tables we have several aria items we can use to create a table from non-table elements.

The only real downside of this method is the fact that you would either have to use JavaScript to add the aria attributes (less preferable) or add them in the HTML markup (better as then even if JavaScript fails it is still perfectly accessible).

So which method would I choose?

I am quite surprised I am saying this but option 1. "Create a second table for mobile view and switch them out using media queries."

Maintainability wise I would assume you would build this programatically linked to a CMS or similar so I don't think that is an issue. It is accessible (assuming table 2 is correctly structured) and it requires minimal CSS.

Other than my gut reaction of this being a hack, I actually think it is a great option and it would be the most accessible way of doing this.

When I get chance to put all the fiddles together you can choose for yourself but I hope this gives you some inspiration in the mean time.

Community
  • 1
  • 1
GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
  • Option 3 seemed to me the most coherent, sticks to an rwd approach and avoids 'avearage strategies' to keep semantic coherent while looking for a strucure update through screen resoutions. after all , the HTML table markup do not seem your best friend here , just the idea that hitches me the most ;) – G-Cyrillus Mar 21 '20 at 21:47
1

I make an answer from the comments ant to give someone else something to use for a better or efficient answer:

included in the snippet below,

This is not all working smoothly together, it is only thoughts, unfinished. Feel free to improve it.

// data attribute generated from the th content of each tr
for (let tr of document.querySelectorAll("tr")) {
  var myDataAttr = tr.querySelector("th").textContent;
  for (let td of tr.querySelectorAll("td")) {
    td.setAttribute("data-time", myDataAttr);
  }
}

// check if display:contents is avalaible
var supported = false;
if (window.CSS) {
    supported = window.CSS.supports('display', 'contents');
} else {
    //nothing needed for now
}


// check screen size for the role attributes on td's 
window.onload = mymq;
window.onresize = mymq;

function mymq() {
  const mq = window.matchMedia("(max-width: 768px)");
  if (mq.matches  &&  supported !=false) {// also checking on display:contents supports
    for (let td of document.querySelectorAll("td")) {
      td.setAttribute("role", "row");
    }
  } else {
    for (let td of document.querySelectorAll("td")) {
      td.setAttribute("role", "cell");
    }
  }
}
table {
  width: 100%;
  border-collapse: collapse;
  background: rgb(196, 215, 70)
}

tr:nth-child(2n) {
  background: lightblue;
}

th,
:before {
  background: tomato;
  box-shadow: inset 0 0 0 2px;
}

th,
td {
  box-shadow: inset 0 0 0 2px;
  text-align: center;
  vertical-align: middle;
  padding: 0.5em;
  vertical-align: middle;
}

@supports (display: contents) {
  /* trick works if data-time attributes stands in html and if display:contents is supported */
  @media screen and (max-width: 768px) {
    table {
      display: flex;
      flex-flow: column;
    }
    thead,
    tr,
    tbody {
      display: contents;
    }
    tr th:first-child {
      display: none;
    }
    th {
      background: red;
    }
    td {
      display: table;
      table-layout: fixed;
      border-collapse: collapse;
      width: 100%;
    }
    td:before {
      content: attr(data-time);
      border-right: solid 1px;
      display: table-cell;
      vertical-align: middle;
      white-space: pre;
      /* only if you care */
      padding: 0.25em;
    }
    tr :nth-child(2) {
      order: 0;
    }
    tr :nth-child(3) {
      order: 1;
    }
    tr :nth-child(4) {
      order: 2;
    }
    tr :nth-child(5) {
      order: 3;
    }
    tr :nth-child(6) {
      order: 4;
    }
    tr :nth-child(7) {
      order: 5;
    }
  }
}

/* let's see if role attribute value is being updated  */
td::after {
  content:'role='attr(role);
  display:block;
  font-family:courier;
  font-size:0.7em;
}
<table role="table">
  <caption>Delivery slots:</caption>
  <tr>
    <td></td>
    <th scope="col">Monday</th>
    <th scope="col">Tuesday</th>
    <th scope="col">Wednesday</th>
    <th scope="col">Thursday</th>
    <th scope="col">Friday</th>
  </tr>
  <tr>
    <th scope="row">09:00 - 11:00</th>
    <td>Closed</td>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
  <tr>
    <th scope="row">11:00 - 13:00</th>
    <td>Open</td>
    <td>Open</td>
    <td>Closed</td>
    <td>Closed</td>
    <td>Closed</td>
  </tr>
</table>
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
G-Cyrillus
  • 101,410
  • 14
  • 105
  • 129
  • This is the way I was thinking, using WAI-ARIA roles, just needs `role="table"` and `role="rowgroup" and `role="columnheader"` in the appropriate places. Thanks for the fiddle, saves a lot of time at my side adding those last few bits in. I couldn't come up with a CSS only way that maintained accessibility :-( – GrahamTheDev Mar 21 '20 at 19:42
  • @GrahamRitchie yep , i got rightaway stuck with : `role="columnheader"`` because i could not figure a way to set it to a pseudo element but for a row. Looking forwards to your final answer. This topic is really one of a great kind to my taste ;) – G-Cyrillus Mar 21 '20 at 21:24