The Dark Side Of The Grid (and Flexbox)

The Dark Side Of The Grid

Introduction

Putting constraints in place is one of the most powerful principles of Software Engineering. When constraints are selected correctly, they allow you to develop considerably faster while also avoiding a wide range of mistakes.

In the case of the CSS Grid and Flexbox, there are a few features that dangerously disconnect visual order from DOM order. This creates an accessibility problem and here's why.

When visual order and DOM order are disconnected

When visual order and DOM order are disconnected this creates a frustrating experience in 3 circumstances:

  • If there are focusable elements in the DOM, you can't predict where focus will go next.

  • You'll annoy users of screen magnifiers when the enlarged portion of the screen unpredictably skips around.

  • If a blind user is working with a sighted user who reads the page in visual order, this will confuse the blind user who encounters information in a different order.

So, long story short, if you want your website or app to remain accessible, add this constraint: do not disconnect visual order from DOM order.

Unaccessible CSS features

So what CSS features create this disconnect? Here's the list.

  • Grid's grid-auto-flow: row dense and grid-auto-flow: col dense

  • Out of order grid-template-areas

  • Flexbox's flex-direction: row-reverse and flex-direction: col-reverse

  • Out of order position: absolute

Let's go through each, one by one.

grid-auto-flow

When you set grid-auto-flow: row dense or grid-auto-flow: col dense, you're asking CSS to use an auto-placement algorithm that attempts to fill in holes earlier in the grid if smaller items come up later.

This changes the visual order of your items disconnecting it from the DOM order.

Here's how that would look like without dense.

GridWithoutDense.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
<div className="mt-8 grid grid-cols-3 grid-rows-3 gap-4">
<div className="row-end-[span_3] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">1</button>
</div>
<div className="row-end-[span_1] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">2</button>
</div>
<div className="row-end-[span_3] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">3</button>
</div>
<div className="col-end-[span_2] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">4</button>
</div>
<div className="row-end-[span_2] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">5</button>
</div>
<div className="rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">6</button>
</div>
</div>

Click anywhere on this paragraph and press Tab. Notice how the focus order is correct. Also take into account that this accessibility problem remains true even if there is nothing to focus on, i.e. no buttons. Screen readers will still read things in DOM order.

And here's how that would look like with dense.

GridWithDense.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
<div className="mt-8 grid grid-flow-dense grid-cols-3 grid-rows-3 gap-4">
<div className="row-end-[span_3] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">1</button>
</div>
<div className="row-end-[span_1] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">2</button>
</div>
<div className="row-end-[span_3] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">3</button>
</div>
<div className="col-end-[span_2] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">4</button>
</div>
<div className="row-end-[span_2] rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">5</button>
</div>
<div className="rounded-md bg-green-200">
<button className="h-full w-full text-center align-middle">6</button>
</div>
</div>

Click anywhere on this paragraph and press Tab. Notice how the focus order is wrong, because visual order was changed.

Therefore, grid's auto-placement algorithm is a no-go.

grid-template-areas

Using grid-template-areas isn't necessarily a no-go. In fact, most of the time it's totally ok to use. You just need to be mindful of the fact thatgrid-template-areas describes your layout visually, so you need to keep it in sync with your DOM order.

Here's an example of how we could screw this up.

ScrewingGridTemplateAreas.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
<div className="mt-8 grid grid-cols-2 gap-4"
style={{
gridTemplateAreas: `'header header'
'content sidebar'
'footer footer'`,
}}>
<div className="rounded-md bg-green-200" style={{ gridArea: 'footer' }}>
<button className="h-full w-full text-center align-middle">footer</button>
</div>
<div className="rounded-md bg-green-200" style={{ gridArea: 'sidebar' }} >
<button className="h-full w-full text-center align-middle">sidebar</button>
</div>
<div className="rounded-md bg-green-200" style={{ gridArea: 'content' }} >
<button className="h-full w-full text-center align-middle">content</button>
</div>
<div className="rounded-md bg-green-200" style={{ gridArea: 'header' }} >
<button className="h-full w-full text-center align-middle">header</button>
</div>
</div>

Click anywhere on this paragraph and press Tab. Notice how the focus order is wrong, because visual and DOM order have been disconnected.

flex-direction

flex-direction: column-reverse and flex-direction: row-reverse can also create this disconnect.

Here's an example of how we could screw this up.

FlexDirectionScrewUp.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
<div className="flex flex-row-reverse justify-between gap-4">
<div className="rounded-md bg-green-200">
<button className="h-full px-10 text-center align-middle">1</button>
</div>
<div className="rounded-md bg-green-200">
<button className="h-full px-10 text-center align-middle">2</button>
</div>
<div className="rounded-md bg-green-200">
<button className="h-full px-10 text-center align-middle">3</button>
</div>
<div className="rounded-md bg-green-200">
<button className="h-full px-10 text-center align-middle">4</button>
</div>
</div>

Click anywhere on this paragraph and press Tab. Notice how the focus order is wrong, because the visual order was disconnected from DOM order.

And here's the same problem with column-reverse.

FlexDirectionScrewUp.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
<div className="flex flex-col-reverse gap-4">
<div className="rounded-md bg-green-200">
<button className="w-full text-center align-middle">1</button>
</div>
<div className="rounded-md bg-green-200">
<button className="w-full text-center align-middle">2</button>
</div>
<div className="rounded-md bg-green-200">
<button className="w-full text-center align-middle">3</button>
</div>
<div className="rounded-md bg-green-200">
<button className="w-full text-center align-middle">4</button>
</div>
</div>

Click anywhere on this paragraph and press Tab.

position: absolute

Finally, there's position: absolute. This is one I've personally done thoughlessly for a long time. The thing to keep in mind with position: absolute is that it may also change visual order, so you just need to change the order in the DOM to match the visual order.

Following Radix UI's dialog component, you'd want the cancel button to be the last one to have focus. Here's a correct example. Notice how the cancel button is the last one to have focus.

Here's a correct example.

FlexDirectionScrewUp.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
<div className="relative inline-flex flex-col items-start rounded-lg border p-8">
<Heading size="xl">Edit profile</Heading>
<P size="md">Make changes to your profile. Click save when ready.</P>
<div className="mt-8 flex gap-4">
<div className="flex flex-col items-end gap-4">
<label className="my-auto">Name</label>
<label className="my-auto">Username</label>
</div>
<div className="flex flex-col justify-center gap-4">
<input type="text" className="rounded-md p-2" defaultValue="André Casal" />
<input type="text" className="rounded-md p-2" defaultValue="@andrecasal" />
</div>
</div>
<button className="mt-8 self-end rounded-md bg-muted-50 px-8 py-4">Save changes</button>
<button className="absolute right-2 top-2 rounded-md bg-muted-50 p-4">Cancel</button>
</div>

Notice how the cancel button is the last one to have focus.

Edit profile

Make changes to your profile. Click save when ready.

And here's what most developers incorrectly do - including myself for a long time.

FlexDirectionScrewUp.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
<div className="relative inline-flex flex-col items-start rounded-lg border p-8">
<button className="absolute right-2 top-2 rounded-md bg-muted-50 p-4">Cancel</button>
<Heading size="xl">Edit profile</Heading>
<P size="md">Make changes to your profile. Click save when ready.</P>
<div className="mt-8 flex gap-4">
<div className="flex flex-col items-end gap-4">
<label className="my-auto">Name</label>
<label className="my-auto">Username</label>
</div>
<div className="flex flex-col justify-center gap-4">
<input type="text" className="rounded-md p-2" defaultValue="André Casal" />
<input type="text" className="rounded-md p-2" defaultValue="@andrecasal" />
</div>
</div>
<button className="mt-8 self-end rounded-md bg-muted-50 px-8 py-4">Save changes</button>
</div>

Notice how the cancel button is, incorrectly, the first one to have focus.

Edit profile

Make changes to your profile. Click save when ready.

Conclusion

In conclusion, avoid disconnecting visual order from DOM order because this breaks accessibility and usability patterns. This pertains to grid-auto-flow, incorrect use of grid-template-areas, flex-direction, and incorrect use of position: absolute.

All of these accessibility and usability concerns are encoded in andrecasal/ui- I've purposely removed these options from the <Grid /> and <Flex /> components. As for position: absolute, that's up to you to implement correctly.

If you liked this article, you'll love my newsletter.

Golden nuggets of in-depth code knowledge. Delivered to your inbox every 2 weeks.

Guide to full-stack web development

Once you subscribe you'll get my free guide to modern full-stack web development and solve analysis paralysis from choosing which tools to use.

 

Got it, thanks!

“I thought the website was good. But the newsletter? Even better!”

Keeran Flanegan