88 | The title for this page came from a slip of my tongue. 89 | I actually had wanted to say "responsive and accessible" web applications, 90 | but somehow "responsible" slipped out. 91 |
92 |93 | Regardless of how I came up with the term, I do consider it to be fitting 94 | because I feel that we as developers have a responsibility to ensure that 95 | our application is responsive and accessible. 96 | If we don't, who will? 97 |
98 |99 | It is extremely difficult and expensive to add responsiveness and accessibility after the fact. 100 | For this reason, we need to take them into account 101 | from the very beginning. 102 |
103 |104 | Luckily, with modern HTML and CSS, we can create responsive and 105 | accessible web apps with relative ease. In my years of doing software development, 106 | I have learned some HTML and CSS tips and tricks, and I want to present these in 107 | this post. This list is not exhaustive, but these are tried and true patterns that 108 | I frequently use in different projects. 109 |
110 |Responsive Web Design
113 | 114 |Do we really need responsive web design for our web application? We will only use our web application on desktop computers!
115 | 116 |117 | I've heard this argument many times, but in the long term, it has never turned out to be true. 118 | For this reason, I've created Joy's two laws of web development: 119 |
120 | 121 |Joy’s First Law of Web Development
122 |123 |126 | 127 | 124 |There is no such thing as a non-responsive web application
125 |
Your web application WILL be opened in a mobile phone or tablet at some time in the future and your users WILL expect it to work correctly.
128 | 129 |Joy’s Second Law of Web Development
130 |131 |134 | 135 | 132 | Any work you do now to ensure that your web application behaves responsively WILL be appreciated in the future. 133 |
When it comes to the technical implementation of responsive design, there are two main categories of components that we need to develop:
136 |-
137 |
- Responsive Layout Containers 138 | 139 |
- Squishy Components 140 |
I want to cover these two aspects in the next sections
142 | 143 |Responsive Layout Containers
144 |145 | We firstly need to make sure that we put a lot of thought into designing layout containers which adjust themselves based on the size of our viewport. 146 |
147 | 148 |149 | In the following demo, we can see how we can define a layout conceptually using different grid areas. 150 |
151 |196 | The demo shows five content areas: Header, Sidebar, Main Content, Second Sidebar, and Footer. 197 | On a mobile device, these areas are shown vertically stacked. 198 | On larger viewports, the example layout now has three rows and three columns. 199 | The Header area spans the full width of the top of the viewport in the first row. 200 | In the middle row, the Sidebar, Main Content, and Second Sidebar areas are positioned 201 | next to each other in a three column layout, with the Main Content area expanding 202 | to take up as much space as it can. The Footer area then spans the full width of 203 | the viewport in the bottom row of the layout. 204 |
205 |How can we implement this? To do this, I like to use the following technique.
208 |Explicit CSS Grid Layout with Breakpoints
209 | 210 |211 | Using CSS Grid, I like to use the following technique to declaratively define a default CSS grid for our content. 212 | This is then the CSS which is used for smaller devices. 213 |
214 | 215 |CSS for Mobile Devices
216 | 217 |.layout {
218 | display: grid;
219 | grid-template-areas:
220 | "header"
221 | "sidebar"
222 | "main"
223 | "sidebar-right"
224 | "footer";
225 | grid-template-rows: auto auto 1fr auto auto;
226 | }
227 | .layout > header {
228 | grid-area: header;
229 | }
230 | .layout > aside:nth-of-type(1) {
231 | grid-area: sidebar;
232 | }
233 | .layout > main {
234 | grid-area: main;
235 | }
236 | .layout > aside:nth-of-type(2) {
237 | grid-area: sidebar-right;
238 | }
239 | .layout > footer {
240 | grid-area: footer;
241 | }
242 |
243 |
244 |
245 | Note that this markup explicitly references the
246 | main,
247 | header,
248 | footer, and
249 | aside semantic HTML elements in the CSS code.
250 | This was intentional here, because it forces us to then use the semantic HTML elements and add important landmarks to our web application which improves its accessibility.
251 | With the CSS child combinator operator (>
) we ensure that this element targeted is the direct child of the parent with my layout class (which we would probably add directly to our HTML body).
252 |
CSS for tablets or larger devices
255 | 256 |
257 | Since we have already defined the grid-areas
for our HTML elements, we can now declaratively change the layout using a relatively short CSS snippet within a media query:
258 |
@media (min-width: 40rem) { /* Breakpoint Tablet portrait up */
261 | .layout {
262 | grid-template-areas:
263 | "header header header"
264 | "sidebar main sidebar-right"
265 | "footer footer footer";
266 | grid-template-rows: auto 1fr auto;
267 | grid-template-columns: 20% 1fr 20%;
268 | }
269 | }
270 |
271 |
272 | 273 | Note that when you are defining your breakpoints for your application, 274 | you should consider the correct way to do CSS breakpoints. 275 |
276 | 277 |Squishy Components
278 | 279 |280 | After we have a responsive layout, the next step is to make sure that all of our components are "squishy". 281 | This means that when we place a component into a designated area of a layout, 282 | it should never push itself outside of its designated area. 283 | Instead it should "squish" down to fit inside of the available space. 284 |
285 |286 | This is especially important because our layout container assumes that all of its 287 | content is going to fit, and if this assumption is not true, this could cause the 288 | whole layout to be wider than the viewport and make it necessary for the user to scroll 289 | horizontally. 290 |
291 | 292 |Flexbox + Flex Wrap
293 | 294 |.container {
295 | display: flex;
296 | flex-wrap: wrap;
297 | }
298 |
299 |
300 |
301 | We can use the display: flex;
rule to make the container
302 | a flexbox and
303 | then use the flex-wrap: wrap;
rule to wrap content within the flexbox.
304 |
306 | When there is not enough space for the items to be placed horizontally, 307 | they will begin to wrap and be shown stacked vertically instead. 308 |
309 |flex-wrap
to wrap content in a flexbox.
351 | 352 | The demo shows a flexbox containing two blocks: a block containing a title and a description, 353 | and a block containing a price, time, and amount. 354 | On a larger devices, these block can be positioned next to each other horizontally. 355 | On smaller devices with flex-wrap activated, the second block containing the price, time and amount 356 | will wrap onto the next line and be positioned underneath the first block. 357 |
358 |Nested Flexboxes
362 |363 | It is also possible to nest flexboxes inside each other to create more advanced responsive behavior. 364 | This next example shows the same flex container from our last example, but highlights the 365 | inner flexbox instead of the outer flexbox. 366 |
367 |391 | The demo shows the same demo as the one in the previous example, but in this instance, 392 | the price, time, and amount items are highlighted because they are also contained within 393 | an inner flexbox. This adds an extra layer to the responsiveness: when the viewport gets 394 | so small that there is no longer space for these three items to be positioned next to each 395 | other horizontally, they will begin to wrap as well. 396 |
397 |Intrinsic Grid
401 |.container {
402 | display: grid;
403 | grid-template-columns:
404 | repeat(auto-fill, minmax(var(—col-width), auto));
405 | }
406 |
407 |
408 |
409 | We can use the CSS repeat function
410 | with grid-template-columns
in order to create an intrinsic grid which will generate as
411 | many columns as fit in the given space. An example using this type of grid is available in
412 | my demo for live-coding css layout.
413 |
440 | The demo shows a grid with 6 different content blocks that are positioned within an intrinsic CSS grid. 441 | On mobile devices, there is only enough space for a single column, so the blocks are all positioned within 442 | this column. When there is enough space for two columns, the layout will shift and the elements will be 443 | positioned within the grid in two columns and end up filling three rows. 444 | When there is enough space for three columns, 445 | the layout will shift again and the elements will be positioned within the grid in three columns and will 446 | end up filling two rows. This layout pattern will continue, and a grid will always be generated with 447 | as many columns as fit within the viewport size. 448 |
449 |Horizontal Scrolling
453 | 454 |.horizontal-scroll {
455 | overflow-x: auto;
456 | }
457 |
458 |
459 | 460 | When we talk about creating an application which is responsive, this doesn't mean that we need to optimize 461 | all of our layouts for small device screens. In certain contexts, we will have larger data representations 462 | (e.g. tables or code examples), which we also want to be available for smaller devices even if they are not 463 | optimized for that layout. 464 |
465 |466 | For this, I like to use a wrapper around tables and code examples which make them able to be scrolled horizontally 467 | when there is not enough space for them in the current layout. 468 |
469 | 470 |531 | In the example, a large table with twenty columns is shown. On smaller devices, there is 532 | not enough space for the whole table to be shown. In this case, the large table is shown 533 | within a container that fits within the viewport, and the user can scroll within that 534 | container in order to view all of the contents. 535 |
536 |Squishy Text
540 | 541 |.squishy-text {
542 | word-break: break-word; /* Samsung browser */
543 | word-wrap: break-word; /* IE 11 */
544 | overflow-wrap: anywhere;
545 | -webkit-hyphens: auto;
546 | -ms-hyphens: auto;
547 | hyphens: auto;
548 | }
549 |
550 |
551 | 552 | The following example is a CSS meme now. 553 | By default, long words in an HTML document will not be hyphenated by default, 554 | so they will break out of their containing box instead of squishing to fit inside of it. 555 | This is especially important when we are dealing with a language which has a lot of long words 556 | (*cough* German *cough*). 557 |
558 |559 | The previous CSS snippet is one I have successfully used to make my text squishy in different contexts. 560 |
561 |Accessible Web Design
574 | 575 |576 | We now come to accessibility. 577 | Here I feel that I can only scratch the surface of the different things that we should consider. 578 | I also am learning new things all the time, so I'm sure that this list is not exhaustive. 579 | But I do think it is a good starting off point. 580 |
581 | 582 |Headings
583 |Don’t skip heading levels.
584 |585 | In your HTML Document, you need to ensure that your document hierarchy is complete and doesn't skip levels. 586 | Otherwise, users who depend on assistive technologies will get confused because it will seem like content is 587 | missing. 588 |
589 |
590 | This is a mistake that many developers make,
591 | because we pay attention to how the heading appears visually without making sure that an h2
is always
592 | directly followed by a h3
.
593 |
Landmarks
596 |597 | We should make sure that we use elements like 598 | main and 599 | header 600 | because then we get HTML landmarks 601 | out of the box. 602 | This makes the page much easier to navigate. 603 |
604 |
605 | Note that the main
element is not well supported for IE11, so if you have to
606 | support older browsers, you should also consider using skip links.
607 |
nav Element
610 |611 | When you are providing links for a user to navigate within your page (e.g. a navbar or a table of contents), 612 | you should wrap them in a nav. 613 |
614 | 615 |label Element
616 |617 | Always add a label to 618 | let assistive technologies know which data is supposed to be entered in an input field. 619 |
620 | 621 |<label>
622 | First Name
623 | <input type="text" value="name" placeholder="Jane" />
624 | </label>
625 |
626 |
627 |
628 | Here we either wrap the input field directly in the label,
629 | or we can use the for
attribute and link it to a specific input field.
630 |
632 | Here it is important to not use the placeholder
attribute to label the input field.
633 | The placeholder attribute should be used to show an example of how the data we expect should appear.
634 | This is particularly important for users who have congnitive disabilities, because when the instructions
635 | in the placeholder disappear, they may not remember what they were supposed to enter into the field.
636 | Please also read this article about why placeholder are problematic.
637 |
List Elements
640 |641 | Using lists (an unordered list, 642 | an ordered list, or a 643 | description list) for things in a UI will also add extra context for assistive technologies. 644 | For instance, it will tell assistive technologies how many elements are contained in a list. 645 |
646 |
647 | In practice, description lists can be difficult to style because we are not allowed to add an extra div
as a wrapper around the dt
and dd
elements.
648 | For this reason, I've also done some experiments about how to best group information in the UI.
649 | Here there isn't a single correct solution.
650 | You will have to find out what works best for your UI.
651 |
654 | Update: Originally, I discussed having difficulties styling description lists.
655 | This statement is no longer correct for modern browsers which implement the
656 | HTML 5.2 Recommendation
657 | because they now allow a div
as a wrapper around the dt
and dd
elements.
658 |
Accordions
661 | 662 |663 | If we need an accordion, or an element which shows/hides a content area, 664 | we can use the HTML details element. 665 |
666 | 667 |<details>
668 | <summary>Toggle Button</summary>
669 | Content which will be expanded/collapsed when clicking the
670 | summary element
671 | </details>
672 |
673 |
674 |
675 | We can also implement this in JavaScript using a button
and the
676 | aria-expanded
attribute.
677 | The aria-expanded
attribute adds context information to the button which will tell the screenreader
678 | if the area that the button is toggling is currently collapsed or expanded.
679 |
681 | I have seen incorrect implementations of this where the developer thought that the attribute was intended to be added to 682 | the content block which is expanded or collapsed, but this is not the case and that implementation would be confusing to any user of an assistive technology. 683 | If we do spend the effort to set aria roles in our application (which we should), 684 | we need to test our application and make sure they are used correctly! 685 | No aria usage is better than incorrect aria usage. 686 |
687 |688 | I have implemented this behavior many times, and when I do, I prefer to use a custom element and activate 689 | the toggle with progressive enhancement. 690 | This means that my toggle button is hidden by default before JavaScript is activated, and the content area that is to be collapsed/expanded 691 | will only be hidden once JavaScript is activated. 692 |
693 |
694 | The contract for the toggle-button
component that I usually end up writing therefore usually looks something like this:
695 |
<button is="toggle-button" data-target="#section2"
698 | aria-expanded="false" hidden>
699 | Toggle Section 2
700 | </button>
701 |
702 |
703 | aria-expanded is an attribute of the BUTTON which tells assistive technologies if the content area that is being expanded/collapsed is currently visible or not.
704 | 705 |
706 | And my toggle-button
implementation usually looks a lot like the following
707 | (I really need to standardize this and publish a custom element one of these days...):
708 |
class ToggleButton extends HTMLButtonElement {
711 | connectedCallback () {
712 | this.removeAttribute("hidden");
713 | if (this.getAttribute("aria-expanded") !== "true") {
714 | this.setAttribute("aria-expanded", "false");
715 | this.target.classList.add("hide");
716 | }
717 | this.addEventListener("click", this.toggle.bind(this));
718 | }
719 |
720 | toggle () {
721 | let classList = this.target.classList;
722 | if (classList.contains("hide")) {
723 | classList.remove("hide");
724 | this.setAttribute("aria-expanded", "true");
725 | } else {
726 | classList.add("hide");
727 | this.setAttribute("aria-expanded", "false");
728 | }
729 | }
730 |
731 | get target () {
732 | return document.querySelector(this.getAttribute("data-target"));
733 | }
734 | }
735 | customElements.define("toggle-button", ToggleButton, { extends: "button" });
736 |
737 |
738 | Here is a code demo of this component.
739 | 740 |aria-label and aria-labelledby
741 |742 | Sometimes we have visual elements which add information to the UI, 743 | but are missing in the UI. For these cases, we can use 744 | an aria-label attribute 745 | with a textual description of what the visual element is showing. If there is already a UI 746 | element available, we can also use aria-labelledby and reference the HTML id of 747 | the existing element. 748 | The HTML title 749 | attribute is not well supported by assistive technologies, so it should not be used to do this. 750 |
751 |752 | Here it really helps to test your UI in a screenreader so that you can figure out where 753 | extra labels would be helpful 😉. 754 |
755 | 756 |Hiding content visually (but not from screenreaders)
757 | 758 |.visually-hidden {
759 | clip: rect(0 0 0 0);
760 | clip-path: inset(50%);
761 | height: 1px;
762 | overflow: hidden;
763 | position: absolute;
764 | white-space: nowrap;
765 | width: 1px;
766 | }
767 |
768 |
769 |
770 | Sometimes we want to hide elements from our UI.
771 | If we use a CSS rule like display: none;
772 | this not only hides the element visually,
773 | but also hides it from assistive technologies.
774 | We can use the previous CSS snippet (or one like it),
775 | to hide the content visually without removing it from
776 | the accessibility tree.
777 |
Hiding content from assistive technologies
780 |781 | In the same vein, sometimes we add visual elements to our UI which are not necessary 782 | for assistive technologies and could potentially cause confusion or unnecessary noise. 783 | This could be the case, for instance, when we add icons to our visual design in addition 784 | to existing textual labels. We may then want to hide these from assistive technologies 785 | using the aria-hidden attribute. 786 |
787 | 788 |Descriptive alt text for images
789 |790 | When using images in a user interface, we need to set 791 | the alt attribute for the image. 792 | This alt-text should provide a description of the same information that we are trying to 793 | convey visually with the image that we have chosen. 794 | This guide provides more information about alt-texts. 795 |
796 | 797 |Adding clear focus styles
798 |799 | By default, the browser will add focus styles to the UI to indicate to users which element on the 800 | screen is currently active. This is especially important for keyboard users, who need the focus styles 801 | in order to correctly navigate throughout your page. 802 |
803 |804 | If you find the default focus styles irritating in your design, you can customize them with the 805 | :focus CSS pseudo-selector. 806 | However, here it is very important that you still provide a focus style which has a high 807 | contrast and is easily identifiable within the UI. 808 |
809 | 810 |Setting focus correctly
811 |
812 | As long as you are writing only HTML and CSS,
813 | there isn't any good reason why you would want to mess with the focus
within your webpage,
814 | because the semantics and structure of HTML is very well designed.
815 | However, when we begin to modify the HTML of our application with client-side JavaScript,
816 | we need to think about the focus and if we need to update it as well.
817 |
819 | For instance, if we are adding content to our DOM with JavaScript, 820 | as I did with this 821 | “Show More” Pagination Example, 822 | we should consider whether we should move the focus to the new content after it has loaded. 823 |
824 |825 | In this case, we need to make absolutely sure that you 826 | test the application with a screenreader and keyboard 827 | to ensure that the focus is set correctly. 828 | There is little that can break your UI more than incorrectly setting the focus. 829 |
830 |833 | I hope you enjoyed this little collection of tips and tricks for creating responsive and accessible web applications. 834 | I also hope you were able to learn something that you can use in your next project. 835 | Let's make a responsible web together. 836 |
837 |838 | If you have suggestions for other useful tricks or tips, please open an issue 839 | on the GitHub Repo for this page 840 | and I will do my best to add it to this list. 841 |
842 |