Learn
- Learn Accessibility
- ARIA and HTML
- Color and contrast
- Conclusion and next steps
- Design and user experience
- Keyboard focus
- Forms
- Glossary
- Images
- JavaScript
- How is digital accessibility measured?
- The Document
- Animation and motion
- Patterns, components, and design systems
- Content structure
- Assistive Technology testing
- Automated accessibility testing
- Manual accessibility testing
- Typography
- Video and audio
- What is digital accessibility, and why does it matter?
- Learn CSS
- Animations
- Backgrounds
- Blend Modes
- Borders
- Box Model
- Color
- Conclusion and next steps
- Filters
- Flexbox
- Focus
- Functions
- Gradients
- Grid
- Inheritance
- Layout
- Lists
- Logical Properties
- Overflow
- Pseudo-classes
- Pseudo-elements
- Selectors
- Shadows
- Sizing Units
- Spacing
- Specificity
- The cascade
- Transitions
- Text and typography
- Z-index and stacking contexts
- Learn Responsive Design
- Accessibility
- Conclusion and next steps
- Icons
- Interaction
- Internationalization
- Introduction
- Macro layouts
- Media features
- Media queries
- Micro layouts
- The picture element
- Responsive images
- Screen configurations
- Theming
- Typography
- User interface patterns
- Learn Forms
- Accessibility
- Address forms
- Form attributes in depth
- Help users avoid re-entering data in forms
- Autofill
- Conclusion and next steps
- Test forms across devices and platforms
- Gathering data
- Design basics
- Form fields in depth
- The form element in depth
- Use forms to get data from users
- Help users enter data in forms
- Identity
- Internationalization and localization
- JavaScript
- Payment forms
- Security and privacy
- Styling forms
- Styling form controls
- Test your forms
- How to test forms for usability
- Help users enter the right data in forms
- Learn HTML
- HTML APIs
- Attributes
- Audio and Video
- Conclusion and next steps
- Details and summary
- Dialog
- Document structure
- Focus
- Forms
- Headings and sections
- Images
- Other inline text elements
- Links
- Lists
- Metadata
- Navigation
- Overview of HTML
- Semantic HTML
- Tables
- Template, slot, and shadow
- Text basics
- Learn Images
- Automating compression and encoding
- Image formats: AVIF
- Image content delivery networks
- Site Generators, frameworks, and CMSs
- Conclusion
- Descriptive syntaxes
- Image formats: GIF
- A brief history of images on the web
- Image formats: JPEG
- Key performance issues
- Image formats: PNG
- Prescriptive syntaxes
- Raster images
- Responsive images
- Vector images
- Image formats: WebP
- Learn Privacy
- Best practices
- Use just the data you need
- Encryption
- Fingerprinting
- Third parties
- Learn PWA
- App design
- Architecture
- Assets and data
- Caching
- Capabilities
- Complexity management
- Conclusion
- Detection
- Enhancements
- Experimental features
- Foundations
- Getting started
- Installation
- Installation prompt
- Offline data
- OS Integration
- Progressive Web Apps
- Service workers
- Serving
- Tools and debug
- Update
- Web app manifest
- Window management
- Workbox
Learn Accessibility
Digital accessibility, commonly abbreviated a11y, is about designing and building websites and web apps that disabled people can interact with in a meaningful and equivalent way.
This course is created for beginner and advanced web developers. You can go through the series from start to finish to get a general understanding of accessibility practices and testing, or you can use it as a reference for specific subjects. If you read an unfamiliar term, refer to our glossary. For those new to web development overall, check out the Learn HTML courses for fundamentals with markup and Learn CSS courses for styling fundamentals.
This is not a complete reference. Each section introduces the section topic, provides context and examples. There will be links to topic references, such as MDN and WCAG specifications, and other web.dev articles. Each section will have a short assessment to help people confirm their understanding.
Some accessibility best practices differ, based on the disability you're addressing. It's important to understand your users and to listen to people with disabilities when they tell you what they need.
We are open to your feedback! Open a GitHub issue and give as much information as you can so we can address it.
Here's what you'll learn:
{% include 'partials/course-index.njk' %}
So, are you ready to learn accessibility? Let's get started.
{% Aside %} This course was written by Carie Fisher (Twitter and LinkedIn), with input and review from Alexandra White (Twitter), and Rachel Andrew (Twitter). Special thanks to Mark Steadman for providing additional support for the JavaScript module, and Aaron Forinton for supporting the publication process. {% endAside %}
ARIA and HTML
Most developers are familiar with the standard markup language of our modern web, HyperText Markup Language (HTML). However, you may be less familiar with Accessible Rich Internet Applications (ARIA) (formally called WAI-ARIA): what it is, how it is used, and when—and when not—to use it.
HTML and ARIA play important roles in making digital products accessible, especially when it comes to assistive technology (AT) such as screen readers. Both are used to convert content into an alternate format, such as Braille or Text-to-Speech (TTS).
Let's review a short history of ARIA, why it is important, and when and how best to use it.
Introduction to ARIA
ARIA was first developed in 2008 by the Web Accessibility Initiative (WAI) group—a subset of the overarching World Wide Web Consortium (W3C), which governs and regulates the internet.
ARIA is not a true programming language but a set of attributes you can add to HTML elements to increase their accessibility. These attributes communicate role, state, and property to assistive technologies via accessibility APIs found in modern browsers. This communication happens through the accessibility tree.
"WAI-ARIA, the Accessible Rich Internet Applications Suite, defines a way to make web content and web applications more accessible to people with disabilities. It especially helps with dynamic content and advanced user interface controls developed with HTML, JavaScript, and related technologies."
The WAI group
The accessibility tree
ARIA modifies incorrect or incomplete code to create a better experience for those using AT by changing, exposing, and augmenting parts of the accessibility tree.
The accessibility tree is created by the browser and based on the standard Document Object Model (DOM) tree. Like the DOM tree, the accessibility tree contains objects representing all the markup elements, attributes, and text nodes. The accessibility tree is also used by platform-specific accessibility APIs to provide a representation that assistive technologies can understand.
ARIA on its own doesn't change an element's functionality or visual appearance. That means only AT users will notice differences between a digital product with ARIA and one without it. That also means that developers alone are responsible for making the appropriate code and styling changes to make an element as accessible as possible.
The three main features of ARIA are roles, properties, and states/values.
Roles define what an element is or does on the page or app.
<div role="button">Self-destruct</div>
Properties express characteristics or relationships to an object.
<div role="button" aria-describedby="more-info">Self-destruct</div>
<div id="more-info">This page will self-destruct in 10 seconds.</div>
States/values define the current conditions or data values associated with the element.
<div role="button" aria-describedby="more-info" aria-pressed="false">
Self-destruct
</div>
<div id="more-info">
This page will self-destruct in 10 seconds.
</div>
While all three elements of ARIA can be used in one line of code, it's not required. Instead, layer ARIA roles, properties, and states/values until you've accomplished your final accessibility goal. Correctly incorporating ARIA into your code base ensures that AT users will have all the information they need to use your website, app, or other digital product successfully and equitably.
Recently, Chrome DevTools has created a way to see the full accessibility tree making it easier for developers to understand how their code impacts accessibility.
When to use ARIA
In 2014, the W3C officially published the HTML5 recommendation. With it came
some big changes, including the addition of landmark elements such as <main>,
<header>, <footer>, <aside>, <nav>, and attributes like hidden and
required. With these new HTML5 elements and attributes, coupled with
increased browser support, certain parts of ARIA are now less critical.
When the browser supports an HTML tag with an implicit role with an ARIA equivalent, there is usually no need to add ARIA to the element. However, ARIA still includes many roles, states, and properties that aren't available in any version of HTML. Those attributes remain useful for now.
This brings us to the ultimate question: When should you use ARIA? Thankfully the WAI group has developed the five rules of ARIA to help you decide how to make elements accessible.
Rule 1: Don't use ARIA
Yes, you read that right. Adding ARIA to an element does not inherently make it more accessible. The WebAIM Million annual accessibility report found that home pages with ARIA present averaged 70% more detected errors than those without ARIA, primarily due to the improper implementation of the ARIA attributes.
There are exceptions to this rule. ARIA is required when an HTML element doesn't have accessibility support. This could be because the design doesn't allow for a specific HTML element or the wanted feature/behavior isn't available in HTML. However, these situations should be scarce.
{% Compare 'better' %}
<button>Submit</button>
{% endCompare %}
When in doubt, use semantic HTML elements.
Rule 2: Don't add (unnecessary) ARIA to HTML
In most circumstances, HTML elements work well out of the box and do not need additional ARIA added to them. In fact, developers using ARIA often have to add additional code to make the elements functional in the case of interactive elements.
Heading tab
``` {% endCompare %}{% Compare 'better' %}
<div role="tab"><h2>Heading tab</h2></div>
{% endCompare %}
Do less work and have better-performing code when you use HTML elements as intended.
Rule 3: Always support keyboard navigation
All interactive (not disabled) ARIA controls must be keyboard accessible. You can add tabindex= "0" to any element that needs a focus that doesn't normally receive keyboard focus. Avoid using tab indexes with positive integers whenever possible to prevent potential keyboard focus order issues.
{% Compare 'better' %}
<span role="button" tabindex="0">Submit</span>
{% CompareCaption %}
Of course, if you can, use a real <button> element in this case.
{% endCompareCaption %}
{% endCompare %}
{% Aside 'caution' %} Remember, people with and without visual impairments use keyboard navigation. Don't add unnecessary tab stops to headings and paragraphs, as these can add additional challenges for some users who navigate by keyboard alone. {% endAside %}
Rule 4: Don't hide focusable elements
Don't add role= "presentation" or aria-hidden= "true" to elements that need
to have focus—including elements with a tabindex= "0". When you add
these roles/states to elements, it sends a message to the AT that these
elements are not important and to skip over them. This can lead to confusion or
disrupt users attempting to interact with an element.
{% Compare 'better' %}
<div><button>Submit</button></div>
{% endCompare %}
Rule 5: Use accessible names for interactive elements
The purpose of an interactive element needs to be conveyed to a user before they know how to interact with it. Ensure that all elements have an accessible name for people using AT devices.
Accessible names can be the content surrounded by an element (in the case of an
<a>), alternative text, or a label.
For each of the following code samples, the accessible name is "Red leather boots."
<!-- A plain link with text between the link tags. -->
<a href="shoes.html">Red leather boots</a>
<!-- A linked image, where the image has alt text. -->
<a href="shoes.html"><img src="shoes.png" alt="Red leather boots"></a>
<!-- A checkbox input with a label. -->
<input type="checkbox" id="shoes">
<label for="shoes">Red leather boots</label>
There are many ways to check an element's accessible name, including inspecting the accessibility tree using Chrome DevTools or testing it with a screen reader.
{% Aside %} Coming soon: read more about screen reader testing in the Assistive Technology module. {% endAside %}
ARIA in HTML
While using HTML elements on their own is best practice, ARIA elements can be added in certain situations. For example, you may pair ARIA with HTML in patterns that need a higher level of AT support because of environmental constraints or as a fall-back method for HTML elements that aren't fully supported by all browsers.
Of course, there are recommendations for implementing ARIA in HTML. Most importantly: don't override default HTML roles, reduce redundancy, and be aware of unintended side effects.
Let's look at some examples.
{% Compare 'worse' %}
<a role="heading">Read more</a>
{% CompareCaption %} Assigned the wrong role. {% endCompareCaption %}
{% endCompare %}
{% Compare 'better' %}
<a aria-label="Read more about some awesome article title">Read More</a>
{% CompareCaption %} Correct role and an extra link description. {% endCompareCaption %}
{% endCompare %}
{% Compare 'worse' %}
<ul role="list">...</ul>
{% CompareCaption %} Redundant role. {% endCompareCaption %}
{% endCompare %}
{% Compare 'better' %}
<ul>...</ul>
{% CompareCaption %} Redundancy removed {% endCompareCaption %}
{% endCompare %}
{% Compare 'worse' %}
<details>
<summary role="button">more information</summary>
...
</details>
{% CompareCaption %} Potential side effects. {% endCompareCaption %}
{% endCompare %}
{% Compare 'better' %}
<details>
<summary>more information</summary>
...
</details>
{% CompareCaption %} No unintended side effects. {% endCompareCaption %}
{% endCompare %}
{% Aside %} One place in particular where ARIA can be useful is in forms. Check out the Learn Forms accessibility module. {% endAside %}
Complexities of ARIA
ARIA is complex, and you should always use caution when using it. While the code examples in this lesson are fairly straightforward, creating accessible custom patterns can quickly get complicated.
There are many things to pay attention to, including but not limited to: keyboard interactions, touch interfaces, AT/browser support, translation needs, environmental constraints, legacy code, and user preferences. A little bit of coding knowledge can be detrimental—or just plain annoying—if used incorrectly. Remember to keep your code simple.
Those warnings aside, digital accessibility is not an all-or-nothing situation—it's a spectrum that allows for some gray areas like this. Multiple coding solutions can be seen as "correct," depending on the situation. What is important is that you keep learning, testing, and trying to make our digital world more open to all.
{% Assessment 'aria' %}
Color and contrast
Have you ever tried to read text on a screen and found it difficult to read due to the color scheme or struggled to see the screen in a very bright or low-light environment? Or maybe you are someone with a more permanent color vision issue, like the estimated 300 million people with color blindness or the 253 million people with low vision?
As a designer or developer, you need to understand how people perceive color and contrast, whether temporary, situationally, or permanently. This will help you best support their visual needs.
This module will introduce you to some accessible color and contrast fundamentals.
Perceiving color
Did you know that objects do not possess color but reflect wavelengths of light? When you see color, your eyes receive and process those wavelengths and convert them to colors.
When it comes to digital accessibility, we talk about these wavelengths in terms of hue, saturation, and lightness (HSL). The HSL model was created as an alternative to the RGB color model and more closely aligns with how a human perceives color.
{% Aside %} In CSS, a color can be specified in many ways, such as using color names, RGB, RGBa, HEX, HSL, HSLa, HSV, or HSVa values. HSLa is similar to HSL, only an alpha value has been added. Alpha is a measurement of opacity and is defined as a number between 0.0 (fully transparent) and 1.0 (fully opaque). {% endAside %}
Hue is a qualitative way to describe a color, such as red, green, or blue, where each hue has a specific spot on the color spectrum with values ranging from 0 to 360, with red at 0, green at 120, and blue at 240.
{% Codepen { user: 'web-dev-codepen-external', id: 'eYrEZqE', height: 350, theme: 'auto', tab: 'result', allow: ['geolocation'] } %}
Saturation is the intensity of a color, measured in percentages ranging from 0% to 100%. A color with full saturation (100%) would be very vivid, while a color with no saturation (0%) would be grayscale or black and white.
{% Codepen { user: 'web-dev-codepen-external', id: 'vYjJKBP', height: 350, theme: 'auto', tab: 'result', allow: ['geolocation'] } %}
Lightness is a color's light or dark character, measured in percentages ranging from 0% (black) to 100% (white).
{% Codepen { user: 'web-dev-codepen-external', id: 'poVrboR', height: 350, theme: 'auto', tab: 'result', allow: ['geolocation'] } %}
Measuring color contrast
To help support people with various visual disabilities, the WAI group created a color contrast formula to ensure enough contrast exists between the text and its background. When these color contrast ratios are followed, people with moderately low vision can read text on the background without needing contrast-enhancing assistive technology.
Let's look at images with a vibrant color palette and compare how that image would appear to those with specific forms of color blindness.
On the left, the image shows rainbow sand with purple, red, orange, yellow, aqua green, light blue, and dark blue colors. On the right is a brighter, multicolored rainbow pattern.
Deuteranopia
Deuteranopia (commonly known as green blind) occurs in 1% to 5% of males, 0.35% to 0.1% of females.
People with Deuteranopia have a reduced sensitivity to green light. This color blindness filter simulates what this type of color blindness might look like.
Protanopia
Protanopia (commonly known as red blind) occurs in 1.01% to 1.08% of males and 0.02% of 0.03% of females.
People with Protanopia have a reduced sensitivity to red light. This color blindness filter simulates what this type of color blindness might look like.
Achromatopsia or Monochromatism
Achromatopsia or Monochromatism (or complete color blindness) occurs very, very rarely.
People with Achromatopsia or Monochromatism have almost no perception of red, green, or blue light. This color blindness filter simulates what this type of color blindness might look like.
Calculate color contrast
The color contrast formula uses the
relative luminance of
colors to help determine contrast, which can range from 1 to 21. This formula
is often shortened to [color value]:1. For example, pure black against pure
white has the largest color contrast ratio at 21:1.
(L1 + 0.05) / (L2 + 0.05)
L1 is the relative luminance of the lighter color
L2 is the relative luminance of the darker colors
Regular-sized text, including images of text, must have a color contrast ratio
of 4.5:1 to pass the
minimum WCAG requirements for color.
Large-sized text and essential icons must have a color contrast ratio of 3:1.
Large-sized text is characterized by being at least 18pt / 24px or 14pt /
18.5px bolded. Logos and decorative elements are exempt from these color
contrast requirements.
Thankfully, no advanced math is required as there are a lot of tools that will do the color contrast calculations for you. Tools like Adobe Color, Color Contrast Analyzer, Leonardo, and Chrome's DevTools color picker can quickly tell you the color contrast ratios and offer suggestions to help create the most inclusive color pairs and palettes.
{% Codepen { user: 'web-dev-codepen-external', id: 'PoeKzWq', height: 450, theme: 'auto', tab: 'result' } %}
Using color
Without good color contrast levels in place, words, icons, and other graphical elements are hard to discover, and the design can quickly become inaccessible. But it's also important to pay attention to how the color is used on the screen, as you cannot use color alone to convey information, actions, or distinguish a visual element.
For example, if you say, "click the green button to continue," but omit any additional content or identifiers to the button, it would be difficult for people with certain types of colorblindness to know which button to click. Similarly, many graphs, charts, and tables use color alone to convey information. Adding another identifier, like a pattern, text, or icon, is crucial to help people understand the content.
Reviewing your digital products in grayscale is a good way to detect potential color issues quickly.
{% Codepen { user: 'web-dev-codepen-external', id: 'rNvzLmd', height: 500, theme: 'auto', tab: 'result' } %}
Color-focused media queries
Beyond checking for color contrast ratios and the use of color on your screen, you should consider applying the increasingly popular and supported media queries that offer the users more control over what is displayed on the screen.
For example, using the @prefers-color-scheme media query, you can create a dark theme, which can be helpful to people with photophobia or light sensitivity. You could also build a high contrast theme with @prefers-contrast, which supports people with color deficiencies and contrast sensitivity.
{% Aside %} There are additional media queries and OS settings to consider for color accessibility, but they are far less supported than the two listed in this module. See the article Operating System and Browser Accessibility Display Modes for more information on the various OS accessibility settings. {% endAside %}
Prefers color scheme
{% BrowserCompat 'css.at-rules.media.prefers-color-scheme' %}
The media query @prefers-color-scheme allows users to choose a light or
dark-themed version of the website or app they are visiting. You can see this
theme change in action by changing your light/dark preference settings and
navigating to a browser that supports this media query. Review the
Mac and
Windows instructions for dark mode.
{% Codepen { user: 'web-dev-codepen-external', id: 'ExLvNOw', height: 300, theme: 'auto', tab: 'css,result', allow: ['geolocation'] } %}
{% Details %}
{% DetailsSummary %} Compare light and dark mode. {% endDetailsSummary %}
{% endDetails %}
Prefers contrast
{% BrowserCompat 'css.at-rules.media.prefers-contrast' %}
The @prefers-contrast
media query checks the user's OS settings to see if high contrast is toggled on
or off. You can see this theme change in action by changing your contrast
preference settings and navigating to a browser that supports this media query
(Mac and Windows contrast mode settings).
{% Codepen { user: 'web-dev-codepen-external', id: 'bGMrgNV', height: 300, theme: 'auto', tab: 'css,result', allow: ['geolocation'] } %}
{% Details %} {% DetailsSummary %} Compare regular and high contrast. {% endDetailsSummary %}
{% endDetails %}
Layering media queries
You can use multiple color-focused media queries to give your users even more
choices. In this example, we stacked @prefers-color-scheme and
@prefers-contrast together.
{% Codepen { user: 'web-dev-codepen-external', id: 'wvjqgaK', height: 300, theme: 'auto', tab: 'css,result', allow: ['geolocation'] } %}
{% Details %} {% DetailsSummary %} Compare both color and contrast. {% endDetailsSummary %}
{% endDetails %}
{% Assessment 'color' %}
Conclusion and next steps
Congratulations, you've made it through Learn Accessibility! At this point, you've learned a lot of best practices for accessibility and gone through multiple rounds of accessibility testing. You may have learned some new terminology or refreshed your memory in our glossary.
We hope you are confident applying these lessons to your websites and web applications.
Additional resources
To continue building your accessibility knowledge, we recommend the following resources:
- Web Accessibility Initiative (WAI)
- Web Content Accessibility Guidelines (WCAG) from the W3C
- WCAG Quick Reference Guide
- Accessibility on MDN
- Deque University Digital Accessibility Courses & Accessibility Reference Library
- Global web accessibility laws
- A11y talks
- List of accessible patterns from Smashing Magazine
- How to fund accessibility work
- A11y automation tracker
- Google developer documentation style guide
More resources can be found throughout the modules, as related to each topic.
Community highlights
We conducted several interviews with accessibility practitioners in 2022. Read about their journey to accessibility and best practices:
- Melanie Sumner told us about her journey into engineering, accessible design, Ember.js, and the importance of funding these efforts.
- Olutimilehin Olushuyi told us about his transition from the law to web development, building accessible community, and creating accessible layouts.
- Albert Kim told us about mental health awareness, building for invisible disabilities, the COGA Accessibility Community Group, and more practical wisdom.
- Elisa Bandy told us about her work for Google's internal documentation, how to prioritize use cases, and building expertise.
Brought to you by
{% include 'partials/authors-stack.njk' %}
Special thanks
Thank you to Aaron Forinton for supporting the publication of several modules and assessments.
Design and user experience
Think about your favorite website or app—what makes it your favorite? Now, think about a website or app you dislike—what do you not like about it? How users interact with your design and their experience on your website and app can vary.
That experience can change based on the time of day, the type of device used, if they've had enough sleep the night before, if they are unwell, if they're using assistive technology, and so much more. With close to eight billion people worldwide, the possibilities of how people use and experience your designs are limitless.
Inclusive design
How can we address all of the potential user needs at once? Enter inclusive design. Inclusive design utilizes a human-centered approach that weaves together inclusivity, usability, and accessibility into one.
And unlike universal design, which focuses on a single design that as many people can use as possible, inclusive design principles center on designing for a specific individual or use case, and then extending that design to others.
There are seven accessibility-focused inclusive design principles:
- Provide comparable experience: Ensure your interface provides an equal experience for all, so people can accomplish tasks in a way that suits their needs without undermining the quality of the content.
- Consider the situation: Make sure your interface delivers a valuable experience to people, regardless of their circumstances.
- Be consistent: Use familiar conventions and apply them in a logical manner.
- Give control: Ensure people can access and interact with content in their preferred way.
- Offer choice: Consider providing different ways for people to complete tasks, especially those that are complex or non-standard.
- Prioritize content: Help users focus on core tasks, features, and information by arranging these elements in the preferred order within the content and layout.
- Add value: Consider the purpose and significance of features and how they improve the experience for different users.
Personas
When developing a new design or feature, many teams rely on user personas to guide them through the process. Personas are fictitious characters that use your digital products, often based on quantitative and qualitative user research.
Personas also offer a quick and inexpensive way to test and prioritize those features throughout the design and development process. They help to focus decisions surrounding site components by adding a layer of real-world consideration to the conversation to help align strategy and create goals focused on specific user groups.
Incorporating disabilities
"People are all different. I can only speak from my experience. When you meet one Deaf person, then you've met one Deaf person—not all of us."
Meryl Evans from the ID24 talk Deaf Tech: Travel Through Time from Past to Future.
Personas can be used as an inclusive design tool when you incorporate people with disabilities into your personas. There are many different ways to do this. You may create disability-specific personas, add disabilities to existing user personas, or even create a persona spectrum to reflect the dynamic reality of situational, temporary, and permanent disabilities.
No matter how you incorporate people with disabilities into your personas, they should not be based on real people or stereotypes. And personas are never a substitute for user testing.
{% Details %} {% DetailsSummary %} Persona: Jane Bennet Check out an example of a persona that supports specific use cases. {% endDetailsSummary %}
- Name: Jane Bennet
- Age: 57 years old
- Location: Essex, United Kingdom
- Occupation: UX engineer
- Disability: Hand tremor from Young Onset Parkinson's disease (YOPD)
- Goals: use speech-to-text input to make adding code suggestions easier; find biking equipment online with minimal keystrokes.
- Frustrations: websites lacking keyboard-only support; apps for design with small areas for touch interaction.
As a UX engineer, Jane designs and builds pages that are vital to keeping her company's site relevant. She supports a lot of team members throughout the day. She's the queen of putting out technical fires, and everyone's go-to in the department when anything goes down unexpectedly.
Losing her fine motor skills to tremors is making it increasingly difficult for her to use a mouse. She's been steadily relying on the keyboard more and more to navigate the web. Jane has always been dedicated to her physical fitness. She loves road racing and BMX. This made it all the more of a blow when she was diagnosed with Young Onset Parkinson's disease last year.
{% endDetails %}
Disability simulators
Use extreme caution when using disability simulators to emulate or supplement your personas.
Disability simulators are a double-edged sword in that they can build sympathy or empathy—it can depend on the individual, the context in which the simulator is used, and many other uncontrollable factors. Many accessibility advocates are against using disability simulator tools and recommend seeking out movies, demos, tutorials, and other content created by people with disabilities, and learning about their experiences first-hand.
"I think we need to be completely honest that any simulation activity does not impact some of the most important understandings we want the sighted to know in their heart and their head. Blindness is not the characteristic that defines us, that the misunderstandings and low expectations about blindness are our biggest obstacle.
Those misunderstandings create artificial barriers that prevent us from fully participating, and those false limitations build into something that holds us back."
Mark Riccobono, President of the National Federation of the Blind
Accessibility heuristics
Consider adding heuristics into your workflow as you build your personas and designs. Heuristics are simple rules for interaction design, introduced in 1990 by Jakob Nielsen and Rolf Molich. These ten principles were developed based on years of experience in the field of usability engineering, and have been used in design and human-computer interaction programs ever since.
Fast-forward to 2019, and the design team at Deque created and shared a new set of heuristics focused on digital accessibility. According to their research, up to 67% of all accessibility bugs for a website or app can be avoided when accessibility is part of the design process. That's a huge impact that can be made before even one line of code is written.
Similar to the original set of heuristics, there are ten accessibility heuristics to consider when planning your design.
- Interaction methods and modalities: Users can efficiently interact with the system using the input method of their choosing (such as a mouse, keyboard, touch, etc.).
- Navigation and wayfinding: Users can easily navigate, find content, and determine where they are at all times within the system.
- Structure and semantics: Users can make sense of the structure of the content on each page and understand how to operate within the system.
- Error prevention and states: Interactive controls have persistent, meaningful instructions to help prevent mistakes, and provide users with clear error states which indicate what the problems are and how to fix them whenever errors are returned.
- Contrast and legibility: Users can easily distinguish and read text and other meaningful information.
- Language and readability: Users can easily read and understand the content.
- Predictability and consistency: Users can predict each element's purpose. It's clear how each element relates to the system as a whole.
- Timing and preservation: Users are given enough time to complete their tasks and do not lose information if their time (i.e., a session) runs out.
- Movement and flashing: Users can stop elements on the page that move, flash, or are animated. Users should not be distracted or otherwise harmed by these elements.
- Visual and auditory alternatives: Users can access text-based alternatives for any visual or auditory content which conveys information.
Once you have a basic understanding of these accessibility heuristics, you can apply it to a persona or design using the accessibility heuristics worksheet and by following the instructions provided. This exercise is more enlightening when you gather multiple perspectives.
An example accessibility heuristic review for the navigation and wayfinding checkpoint could look like the following:
After everyone on the team looks at the page or component and conducts their accessibility heuristic review, the totals are tallied up for each checkpoint. At this point, you can decide how to remedy any found issues or correct any omissions that are paramount to supporting digital accessibility.
Accessibility annotations
Before you hand off your design to the development team, you should consider adding accessibility annotations. Annotations, in general, are used to explain creative choices and describe different aspects of the design. Accessibility annotations focus on areas where developers can make more accessible programmatic choices with the guidance of the design team or an accessibility-focused specialist.
Accessibility annotations can be applied during any stage of the design process, from wireframes to high-fidelity mock-ups. They can include user flows, conditional states, and functionality. They often utilize symbols and labels to streamline the process and keep the design as the main focus.
The following design illustrations examples are from Indeed.com's accessibility annotations kit for Figma.
Depending on your design program, you should have multiple accessibility annotation starter kits to choose from. Or, if you prefer, you can create your own set. In either case, you should decide which information needs to be communicated to the hand-off team and what format works best.
Some areas to consider for accessibility annotations include:
- Color: include contrast ratios of all of the different combinations of colors in the palette.
- Buttons and links: identify default, hover, active, focus, and disabled states.
- Skip links: highlight the hidden/visible design aspects and where they link to on the page.
- Images and icons: add alternative text recommendations for essential images/icons.
- Audio and video: highlight areas/links for captions, transcripts, and audio descriptions.
- Headings: add programmatic levels and include everything that looks like a heading.
- Landmarks: highlight the different sections of the design with HTML or ARIA.
- Interactive components: identify clickable elements, hover effects, focus area.
- Keyboard: identify where the focus should start (alpha stop) and the following tab order.
- Forms: add field labels, helper text, error messages, and success messages.
- Accessible names: identify how assistive technology should recognize the element.
Keyboard focus
Many people use a keyboard—or other software/hardware that mimics the functionality of a keyboard, such as a switch device—as their primary means of navigation. People with temporary physical limitations such as sprained wrist or fine motor disabilities like carpal tunnel, as well as some people without disabilities, may choose to use a keyboard to navigate a page due to personal preference, efficiency, or broken hardware.
Low-vision or blind users may use a keyboard for navigation combined with their magnification or screen reader software. However, they may use different keyboard shortcut commands to navigate a screen than a sighted user would.
Keyboard support for all of these disabilities and circumstances is critical. A large part of keyboard accessibility is centered around focus. Focus refers to which element on the screen currently receives input from the keyboard.
In this module, we concentrate on HTML structure and CSS styling for keyboard and focusable elements. The JavaScript module includes more information on focus management and keystrokes for interactive elements.
Focus order
The elements that a keyboard user can navigate to are called focusable elements. Focus order, also called tab or navigation order, is the order in which elements receive focus. The default focus order must be logical, intuitive, and match the visual order of a page.
For most languages, the focus order starts at the top of the page and ends at the bottom, traveling from left to right. However, some languages are read right to left, so the primary language of the page may warrant a different focus order.
By default, focus order includes naturally focusable HTML elements, such as links, checkboxes, and text inputs. Naturally focusable HTML elements include built-in tab order support and basic keyboard event handling.
You can update the focus order to include any elements that don't normally receive focus, such as non-interactive HTML elements, custom components, or elements with ARIA that overrides the natural focus semantics.
{% Aside %}
Your tab key moves the keyboard focus up the
DOM. shift + tab moves the
focus down the DOM.
{% endAside %}
Tabindex
The focus order begins with elements that have a positive tabindex attribute (if there are any) and moves from the smallest positive number to the largest (such as 1, 2, 3). It then proceeds through elements with a tabindex of zero according to their order in the DOM. Any elements with a negative tabindex are removed from the natural focus order.
When a tabindex
of zero (tabindex="0") is applied to normally unfocusable elements, they are
added into the natural focus order of the page according to the way they appear
in the DOM. However, unlike naturally focusable HTML elements, you must
provide additional keyboard support
for them to be fully accessible.
Similarly, if you have an element that normally is focusable but is
inactive—such as a button that is inoperative until an input field is filled
in—you should add a negative tabindex (tabindex="-1") to this element. Adding
a negative tabindex signals to assistive technologies and keyboards that this
button should be removed from the natural focus order. But don't worry—you can
use JavaScript to add the focus back to the element when needed.
In this example, a tabindex attribute was added to elements that do not
naturally receive focus. The order of the elements was manipulated using
tabindex to illustrate the power it can have on focus order. This is an
example of what not to do!
{% Codepen { user: 'web-dev-codepen-external', id: 'QWxWwNM', height: 300, theme: 'dark', tab: 'html' } %}
{% Aside 'caution' %}
As a general rule, you should avoid positive tabindexes. Giving focus to
non-interactive elements and disrupting the normal focus order may confuse and
frustrate your users. It should be rare that a circumstance warrants adding a
positive tabindex (tabindex= "1") to a non-focusable element.
{% endAside %}
Skip links
Most websites today have a long list of menu links in the page's main header consistent from page to page. This is great for general navigation but can make it difficult for keyboard-only users to easily get to the website's main content without having to tab multiple times.
One way to jump over redundant or unuseful groups of links is to add a skip link. Skip links are anchor links that jump to a different section of the same page, using that section's ID, instead of sending the user to another page on the website or an external resource. Skip links are typically added as the first focusable element a user will encounter when arriving at a website and can be visible or visually hidden until a user tabs to it, depending on what the design calls for.
When a user presses the tab key, and an active skip link is in place, it sends the keyboard focus to the skip link. The user can click the skip link and jump past the header section and main navigation. If they choose not to click the skip link and continue to tab down the DOM, they'll be sent to the next focusable element.
Like all links, it’s important that the skip link includes context about the
link's purpose. Adding the phrase "Skip to main content," lets the user know
where the link is taking them. There are many code options to choose from when
providing additional context to your links, such as
aria-labelledby,
aria-label, or adding it
to the text content of the <a> element, as demonstrated in the example.
Focus indicator
As you just learned, focus order is an important aspect of keyboard accessibility. It's also important to decide how that focus is styled. Because even if the focus order is excellent, how could a user know where they are on the page without the proper styling?
A visible focus indicator is a vital tool in informing a user about where they are at all times on the page. It's especially important for your sighted keyboard-only users.
{% Aside %} WCAG 2.2's new success criterion, Focus Not Obscured (Minimum), will ensure that the focus indicator is not hidden beneath other components. {% endAside %}
Browser default styling
Today, every modern web browser has a different default visual styling that applies to focusable elements on your website or app—some more easily visible than others. As a user tabs through the page, this styling is applied as the element receives keyboard focus.
If you allow the browser to handle the focus styling, it's important to review
your code to confirm that your theme won't override the browser's default
styling. An override is often written as "outline: 0" or "outline: none" in
your stylesheet. This tiny piece of code can remove the browser's default focus
indicator styling, which makes it very difficult for users to navigate your
website or app.
{% Compare 'better' %}
a:focus {
outline: auto 5px Highlight; /* for non-webkit browsers */
outline: auto 5px -webkit-focus-ring-color; /* for webkit browsers */
}
{% endCompare %}
Custom styles
Of course, you can go beyond the default browser style and create a custom focus indicator that complements your theme. When creating a custom focus indicator, you have a lot of freedom to be creative!
A focus indicator shape can take on many forms, be it an outline, a border, an underline, a box, a background change, or some other obvious stylistic change that does not rely on color alone to indicate the keyboard's focus is active on that element.
You could change a focus indicator style to ensure it isn’t lost in the background. For example, when a page has a white background, you could set the button focus indicator to a blue background. When the page has a blue background, you could set that same button focus style to a white background.
You could change the focus element style based on element type. For example, you could use a dotted underline for body links but choose a rounded border for a button element.
{% Aside %}
Depending on how the focus indicator is styled, it may also need to meet the [minimum color contrast](https://www.w3.org/WAI/WCAG21/Techniques/general/G195) requirements against the background.
We recommend adhering to a 3:1 color contrast ratio for all focus indicators. This will meet WCAG 2.2's [Focus Appearance (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance-minimum).
{% endAside %}{% Codepen { user: 'web-dev-codepen-external', id: 'PoaowNz', height: 300, theme: 'dark', tab: 'css' } %}
There is no rule about how many focus indicator styles you have on one page—but be sure to keep it to a reasonable number to avoid unnecessary confusion.
Wrap-up
For a website or app to be considered accessible, everything that can be accessed with a mouse must also be accessed with a keyboard. This module focused on the visual aspect of keyboard accessibility, in particular, focus order and focus indicators.
{% Assessment 'focus' %}
Forms
A form is an element that allows a user to provide data into a field or a group of fields. Forms can be as simple as a single field or as complicated as a multi-step form element with multiple fields per page, input validation, and sometimes a CAPTCHA.
Forms are considered one of the most difficult elements to get right from an accessibility perspective, as they require knowledge of all the elements we have already covered, as well as additional rules specific just to forms. With some understanding and time, you can make an accessible form to suit you and your users.
Forms is the last component-specific module in this course. This module will focus on the form-specific guidelines, but all other guidelines you learned about in the earlier modules, such as content structure, keyboard focus, and color contrast, also apply to form elements.
{% Aside %} Review our Learn Forms course to learn how to create better forms. There is a form accessibility module in that course with additional accessibility-specific form content. {% endAside %}
Fields
The backbone of forms is fields. Fields are small interactive patterns, such as an input or radio button element, that allow users to enter content or make a choice. There is a wide variety of form fields to choose from.
The default recommendation is to use established HTML patterns instead of building something custom with ARIA, as certain features and functionalities—such as field states, properties, and values—are inherently built into the HTML elements, while you have to add custom ARIA manually.
{% Aside %}
If the situation requires you to build custom form fields, be sure to review the ARIA and HTML, Keyboard focus, and JavaScript modules to understand how to make these custom form fields as accessible as possible.
{% endAside %}
ARIA
<div role="form" id="sundae-order-form">
<!-- form content -->
</div>
HTML
<form id="sundae-order-form">
<!-- form content -->
</form>
In addition to choosing the most accessible form field patterns, where applicable, you should also add HTML autocomplete attributes to your fields. Adding autocomplete attributes allows a more fine-grained definition or identification of purpose to user agents and assistive technologies (AT).
Autocomplete attributes allow users to personalize visual presentations,
such as showing a birthday cake icon in a field with the birthday autocomplete
attribute (bday) assigned to it. More generally, autocomplete attributes make
filling out forms easier and quicker. This is especially helpful for people
with cognitive and reading disorders and those using ATs, such as screen
readers.
<form id="sundae-order-form">
<p>Name: <input type="name" autocomplete="name"></p>
<p>Telephone: <input type="tel" autocomplete="tel"></p>
<p>Email address: <input type="email" autocomplete="email"></p>
</form>
Lastly, form fields should not produce contextual changes when they receive focus or user input unless the user has been warned about the behavior before using the component. For example, a form should not be automatically submitted when a field receives focus or once a user adds content to the field.
Labels
Labels inform a user about the purpose of a field, if the field is required, and can also provide information about the field requirements, such as input format. Labels must be visible at all times and accurately describe the form field to users.
One of the foundational tenets of accessible forms is establishing a clear and accurate connection between a field and its label. This is true both visually and programmatically. Without this context, a user might not understand how to fill out the fields in the form.
<form id="sundae-order-form">
<p><label>Name (required): <input type="name" autocomplete="name" required></label></p>
<p><label>Telephone (required): <input type="tel" autocomplete="tel" required></label></p>
<p><label>Email address: <input type="email" autocomplete="email"></label></p>
</form>
Additionally, related form fields, such as a mailing address, need to be programmatically and visually grouped. One method is to use the fieldset/legend pattern to group elements that are similar.
{% Codepen { user: 'web-dev-codepen-external', id: 'zYLqwwQ', height: 400, theme: 'auto', tab: 'html, result' } %}
Descriptions
Field descriptions are similar to labels in purpose in that they are used to give more context to the field and requirements. Field descriptions are not required for accessibility if the labels or form instructions are descriptive enough.
However, there are situations in which adding additional information is useful to avoid form errors, such as relaying information about the minimum length of input for a password field or telling a user which calendar format to use (such as MM-DD-YYYY).
There are many different methods you can use to add field descriptions to your
forms. One of the best methods is to add an
aria-describedby
attribute to the form element, in addition to a <label> element. The screen
reader will read both the description and the label.
If you add the
aria-labelledby
attribute to your element, the attribute value overrides the text within your
<label>. As always, be sure to test the final code with all of the ATs you
plan to support.
{% Codepen { user: 'web-dev-codepen-external', id: 'ExpKmXq', height: 400, theme: 'auto', tab: 'html, result' } %}
Errors
When creating accessible forms, there's a lot you can do to prevent users from making form errors. Earlier in this module, we covered clearly marking-up fields, creating identifying labels, and adding detailed descriptions whenever possible. But no matter how clear you think your form is, eventually, a user will make a mistake.
When a user encounters a form error, the first step is to make the error known. The field where the error occurred must be clearly identified, and the error itself must be described to the user in text.
There are different methods for displaying error messages, such as:
- A pop-up modal, inline near where the error occurred
- A collection of errors grouped in one larger message at the top of the page
Be sure to pay attention to the keyboard focus and ARIA live region options when announcing errors.
Whenever possible, offer the user a detailed suggestion on how to fix the error. There are two attributes available to notify users of errors.
- You can use the HTML required attribute. The browser will supply a generic error message based on the filed validation parameters.
- Or you can use the aria-required attribute to share a customized error message to ATs. Only ATs will receive the message unless you add additional code to make the message visible to all users.
Once a user thinks all of the errors have been resolved, allow them to resubmit the form and provide feedback about the results of their submission. An error message tells a user they have more updates to make, while a success message confirms that they have resolved all errors and successfully submitted the form.
{% Codepen { user: 'web-dev-codepen-external', id: 'abjJYWR', height: 400, theme: 'auto', tab: 'html, result' } %}
{% Aside %}
While WCAG 2.2 is still under development, there are several proposed success criteria that will focus on more accessible form experiences, such as Target Size (Minimum), Consistent Help, Accessible Authentication, and Redundant Entry to be aware of for future projects.
{% endAside %}
{% Assessment 'forms' %}
Glossary
In this document, you'll find common terms and concepts related to accessibility.
A11y
A11y is numeronym for accessibility (a + 11 letters + y). Typically pronounced as "ally" as in "a person or group that provides assistance and support in an ongoing effort, activity, or struggle," giving the term "a11y" multiple meanings.
Accessibility Conformance Report (ACR) {: #acr}
The ACR is the final report presented to a client once a full ACT has been performed. If you need a legally binding version of the ACR, you would utilize a version of the Voluntary Product Accessibility Template (VPAT).
Accessibility Conformance Testing (ACT) {: #act}
ACT is commonly referred to as an accessibility audit. The ACT utilizes various testing methodology and tools: primarily automated, manual, and assistive technology (AT) devices.
ACT is first performed as a baseline metric to gauge general accessibility compliance of a digital product. It's often run multiple times throughout the software product lifecycle to check for changes in the level of conformance against a set of pre-determined accessibility checkpoints or guidelines.
ARIA
ARIA is an acronym for Accessible Rich Internet Applications (formally known as WAI-ARIA—Web Accessibility Initiative Accessible Rich Internet Applications). ARIA is a specification written by the W3C, defining a set of attributes that you can add to HTML elements to support accessibility. These attributes communicate role, state, and property to assistive technologies via accessibility APIs implemented in the accessibility tree in modern browsers.
Assitive technology (AT) {: #at}
AT is hardware and software that can be no-tech (such as a mouthstick), low-tech (such as a keyboard), or high-tech (such as a screen reader). AT is used to help increase, maintain, or improve the capabilities of performing a task for a person with disabilities. AT includes braille keyboards, audio browsers, screen magnifiers, and alternative pointing devices.
Digital accessibility
Digital accessibility is the practice of building digital products in a way that all users, regardless of their disability, will have equal access to the content or functionality of the product.
POUR
POUR is shorthand for Perceivable, Operable, Understandable, and Robust, which are the foundational human-focused principles of WCAG.
Screen reader
A screen reader is a high-tech assistive technology that uses synthetic language to read and navigate digital documents for people with low or no vision, cognitive issues, and other disabilities.
- Desktop
- Mobile
- Screen Reader Keyboard Shortcuts and Gestures
VPAT
VPAT is shorthand for Voluntary Product Accessibility Template. A VPAT is a template to draft an Accessibility Conformance Report (ACR). An ACR clearly states which accessibility standards a product or service meets and warns users about any "accessibility blockers" they may encounter. A VPAT does not mean that a digital product is 100% accessible: it is just a report of the state of a product from an accessibility conformance perspective.
Web Accessibility Initiative (WAI)
The Web Accessibility Initiative (WAI) is a sub-group of the W3C focuses only on digital accessibility.
Web Content Accessibility Guidelines (WCAG) {: #wcag}
Web Content Accessibility Guidelines (commonly referred to as ]= WCAG) is an international set of accessibility standards developed through the W3C in cooperation with individuals and organizations. The goal of WCAG is to provide a single shared standard for digital accessibility that meets the needs of individuals, organizations, and governments worldwide.
Images
Accessible images may seem like a simple topic at first glance—you add some "alt text" to an image, and you are done. But, the topic is more nuanced than some people think. In this section, we'll review:
- How to update the code to make images accessible.
- What information should be shared with users and where to share it.
- Additional ways to improve your images to support people with disabilities.
Image purpose and context
Before even one line of code is written, you must first think about the point of the image and how it will be used. Asking yourself questions about the purpose and context of the image can help you determine how best to convey the information to a person using assistive technology (AT) such as screen readers.
You may ask yourself:
- Is the image essential to understanding the context of the feature or page?
- What type of information is the image trying to convey?
- Is the image simple or complex?
- Does the image elicit emotion or prompt the user to act?
- Or is the image just visual "eye candy" with no real purpose?
A visual flowchart, such as an image decision tree, can help you decide which category your image belongs to.
Try hiding the images on your site or web app using a browser extension or other methods. Then ask yourself: "Do I understand the content that remains?" If the answer is yes, it is most likely a decorative image. If not, the image is instead informative in some way and contextually necessary. Once you determine the image's purpose, you can determine the most accurate way to code for it.
{% Img src="image/VbsHyyQopiec0718rMq2kTE1hke2/cnUw9wOPYGvEpO69nqFe.png", alt="Example image decision tree.", width="632", height="598" %}
Decorative images
A decorative image is a visual element that doesn't add additional context or information that allows the user to better understand the context. Decorative images are supplemental and may provide style rather than substance.
If you decide an image is decorative, the image must be programmatically hidden from ATs. When you program an image to be hidden, it signals to the AT that the image is not needed to understand the page's content, context, or action. There are many ways to hide images, including using an empty/null text alternative, applying ARIA, or adding the image as a CSS background. Below are a few examples of how to hide a decorative image from users.
{% Aside 'caution' %}
Be mindful when making this choice, as "[decorative](https://www.smashingmagazine.com/2021/06/img-alt-attribute-alternate-description-decorative/)" can mean different things to different users. Some AT users want to hear descriptions for every visual on the screen.
Users can choose to skip over your image descriptions if and when they deem them redundant or verbose, but they cannot imagine descriptions that don't exist. When in doubt, add descriptions to your images.
{% endAside %}Empty or null alt
{% Codepen { user: 'web-dev-codepen-external', id: 'QWrMgxg', height: 300, theme: 'auto', tab: 'html,css,result' } %}
An empty/null alternative text attribute differs from a missing alternative text attribute. If the alternative text attribute is missing, the AT might read out the file name or surrounding content to give the user more information about the image.
Role set to presentation or none
{% Codepen { user: 'web-dev-codepen-external', id: 'mdLMwje', height: 350, theme: 'auto', tab: 'html,result' } %}
A role set to presentation or none
removes an element's semantics from exposure to the accessibility
tree. Meanwhile, aria-hidden= "true"
will remove the entire element—and all of its children—from the
accessibility API.
<!-- All of these choices lead to the same result. -->
<img src=".../Ladybug.jpg" role="presentation">
<img src=".../Ladybug.jpg" role="none">
<img src=".../Ladybug.jpg" aria-hidden="true">
Images in CSS
{% Codepen { user: 'web-dev-codepen-external', id: 'GRdvEYo', height: 350, theme: 'auto', tab: 'css,result' } %}
When you add a background image with CSS, a screen reader will not detect the image file. Be sure you want the image to be hidden before applying this method.
Informative images
An informative image is an image that conveys a simple concept, idea, or emotion. Types of informative images include photos of real-world objects, essential icons, simple drawings, and images of text.
If your image is informative, you should include programmatic alternative text describing the purpose of the image. Alternative image descriptions—often abbreviated as "alt text"—give AT users more context about an image and help them better understand an image's message or intent.
Simple alternative descriptions using
<img> elements
are achieved by including the alt attribute, regardless of the file type it
points to, such as .jpg, .png, .svg, and others.
<img src=".../Ladybug_Swarm.jpg" alt="A swarm of red ladybugs are eating the leaves of my prize rose bush.">
{% Codepen { user: 'web-dev-codepen-external', id: 'yLjoXrj', height: 350, theme: 'auto', tab: 'html,result' } %}
When you use <svg> elements inline, however, you need to pay attention to accessibility.
First, since SVGs are semantically coded, AT will skip over them by default.
If you have a decorative image, this is not an issue—the AT will ignore
it as intended. But if you have an informative image, an ARIA role="img"
needs to be added to the pattern for the AT to recognize it as an image.
Second, <svg> elements do not use the alt attribute, so
different coding methods must be
used instead to add alternative descriptions to your informative images.
<svg role="img"...>
<title>Cartoon drawing of a red, black, and gray ladybug.</title>
</svg>
Functional images
A functional image is connected to an action. An example of a functional image is a logo that links to the home page, a magnifying glass used as a search button, or a social media icon that directs you to a different website or app.
Like informative images, functional images must include an alternative description to inform all users of their purpose. Unlike an informative image, each functional image needs to describe the image's action—not the visual aspects.
{% Codepen { user: 'web-dev-codepen-external', id: 'YzLxxPw', height: 350, theme: 'auto', tab: 'html,result' } %}
{% Codepen { user: 'web-dev-codepen-external', id: 'gOzxxbg', height: 350, theme: 'auto', tab: 'html,result' } %}
In the logo example, the image is both informative and actionable as it is both an image that conveys information and behaves as a link. In cases like these, you can add alternative descriptions to each element—but it is not a requirement.
One way to add alternative descriptions to images is through visually hidden text. When you use this method, the text will be read by screen readers because it is in the DOM, but it is visually hidden with the help of custom CSS.
You can see from the code snippet that "Navigate to the homepage" is the wrapper title, and the image alternative text is "Lovely Ladybugs for your Lawn." When you listen to the logo code with a screen reader, you hear both the visual and the action conveyed in one image.
<div title="Navigate to the homepage">
<a href="/">
<img src=".../Ladybug_Logo.png" alt="Lovely Ladybugs for your Lawn"></img>
</a>
</div>
{% Codepen { user: 'web-dev-codepen-external', id: 'qBYXXdW', height: 350, theme: 'auto', tab: 'html,result' } %}
Complex images
A complex image often requires more explanation than a decorative, informational, or functional image. It requires both a short and a long alternative description to convey the full message. Complex images include infographics, maps, graphs/charts, and complex illustrations. As with the other image types, there are different methods you can use to add alternative descriptions to your complex images.
{% Codepen { user: 'web-dev-codepen-external', id: 'NWMvvqw', height: 400, theme: 'auto', tab: 'html,result' } %}
<img src=".../Ladybug_Anatomy.svg" alt="Diagram of the anatomy of a ladybug."><a href="ladybug-science.html">Learn more about the anatomy of a ladybug</a>
One way to add additional explanation to an image is to link out to a resource or provide a jump link to a longer explanation later on the page. This method is a good choice, not only for AT users but also helps people with disabilities—such as cognitive, learning, and reading disabilities—who might benefit from having this additional image information readily available on the screen instead of buried in the code.
{% Codepen { user: 'web-dev-codepen-external', id: 'OJZjxGK', height: 400, theme: 'auto', tab: 'html,result' } %}
Another method you can use is to append the aria-describedby attribute to the
<img> element. You can programmatically link the image to an ID containing a
longer description. This method creates a strong association between the image
and the full description. The extended description can be displayed on the
screen or visually hidden—but consider keeping it visible to support even
more people.
{% Codepen { user: 'web-dev-codepen-external', id: 'MWGvEdJ', height: 450, theme: 'auto', tab: 'html,result' } %}
One other way to group short alternative descriptions with a longer one is to use the HTML5 <figure> and <figcaption> elements. These elements act similarly to aria-describedby in that it semantically groups elements, forming a stronger association between the image and its description. Adding ARIA role="group" ensures backward compatibility with older web browsers that don't support the native semantics of the <figure> element.
Alternative text best practices
Of course, including alternate text is not enough. The text should also be meaningful. For example, if your image is about a swarm of ladybugs chewing the leaves of your prize rose bush, but your alternative text reads "bugs," would that convey the full message and intent of the image? Definitely not.
Alternative descriptions need to capture as much relevant visual information as possible and be succinct. While there is no limit to the number of characters a screen reader can read, it is usually advised to cap your alternative text to 150 characters or less to avoid reader fatigue. If you need to add additional context to the image, you can use one of the complex image patterns, add caption text, or further describe the image in the main copy.
Some additional alternative text best practices include:
- Avoid using words like "image of" or "photo of" in the description, as the screen reader will identify these file types for you.
- When naming your images, be as consistent and accurate as possible. Image names are a fallback when the alternative text is missing or ignored.
- Avoid using non-alpha characters (for example, #, 9, &) and use dashes between words rather than underscores in your image names or alternative text.
- Use proper punctuation whenever possible. Without it, the image descriptions will sound like one long, never-ending, run-on sentence.
- Write alternative text like a human and not a robot. Keyword stuffing does not benefit anyone—people using screen readers will be annoyed, and search engine algorithms will penalize you.
{% Assessment 'images' %}
JavaScript
JavaScript plays a major role in almost everything we create—from smaller dynamic components to full products running on a JavaScript framework, such as React or Angular.
This use (or overuse) of JavaScript has brought forward many alarming trends, such as long load times due to large amounts of code, use of non-semantic HTML elements, and injection of HTML and CSS through JavaScript. And you may be unsure of how accessibility fits into each of these pieces.
JavaScript can have a huge impact on the accessibility of your site. In this module, we'll share some general patterns for accessibility that are enhanced by JavaScript, as well as solutions for accessibility issues that arise from using JavaScript frameworks.
Trigger events
JavaScript events allow users to interact with web content and perform a specific action. Many people, such as screen reader users, people with fine-motor skill disabilities, people without a mouse or trackpad, and others, rely on keyboard support to interact with the web. It's critical that you add keyboard support to your JavaScript actions, as it affects all of these users.
Let's look at a click event.
If an onClick() event is used on a semantic HTML element such as a <button>
or <a>, it naturally includes both mouse and keyboard functionality. However,
keyboard functionality is not automatically applied when an onClick() event
is added to a non-semantic element, such as a generic <div>.
{% Compare 'better' %}
<button onclick="doAction()">Click me!</button>
{% endCompare %}
Preview this comparison on CodePen.
If a non-semantic element is used for a trigger event, a keydown/keyup event must be added to detect the enter or space key press. Adding trigger events to non-semantic elements is often forgotten. Unfortunately, when it's forgotten, the result is a component that's only accessible via a mouse. Keyboard-only users are left without access to the associated actions.
Page titles
As we learned in the Document module, the page title is essential for screen reader users. It tells users what page they are on and whether they have navigated to a new page.
If you use a JavaScript framework, you need to consider how you handle page
titles. This is especially important for
single-page apps (SPAs)
that load from a singular index.html file, as transitions or routes (page
changes) will not involve a page reload. Each time a user loads a new page in
an SPA, the title won't change by default.
For SPAs, the document.title value can be added manually or with a helper package (depending on the JavaScript framework). Announcing the updated page titles to a screen reader user may take some additional work, but the good news is you've got options, such as dynamic content.
Dynamic content
One of the most powerful JavaScript functionalities is the ability to add HTML and CSS to any element on the page. Developers can create dynamic applications based on the actions or behaviors of the users.
Let's say you need to send a message to users when they log in to your website or app. You want the message to stand out from the white background and relay the message: "You are now logged in."
You can use the element innerHTML
to set the content:
document.querySelector("#banner").innerHTML = '<p>You are now logged in</p>';
You can apply CSS in a similar way, with
setAttribute:
document.querySelector("#banner").setAttribute("style", "border-color:#0000ff;");
With great power comes great responsibility. Unfortunately, JavaScript injection of HTML and CSS has historically been misused to create inaccessible content. Some common misuses are listed here:
| Possible misuse | Correct use |
|---|---|
| Render large chunks of non-semantic HTML | Render smaller pieces of semantic HTML |
| Not allowing time for dynamic content to be recognized by assistive technology | Using a setTimeout() time delay to allow users to hear the full message |
Applying style attributes for onFocus() dynamically |
Use :focus for the related elements in your CSS stylesheet |
| Applying inline styles may cause user stylesheets to not be read properly | Keep your styles in CSS files to keep the consistency of the theme |
| Creating very large JavaScript files that slow down overall site performance | Use less JavaScript. You may be able to perform similar functions in CSS (such as animations or sticky navigation), which parse faster and are more performant |
For CSS, toggle CSS classes instead adding inline styles, as this allows for reusability and simplicity. Use hidden content on the page and toggle classes to hide and show content for dynamic HTML. If you need to use JavaScript to dynamically add content to your page, ensure it's simple and concise, and of course, accessible.
Focus management
In the Keyboard focus module, we covered focus order and indicator styles. Focus management is knowing when and where to trap the focus and when it shouldn't be trapped.
Focus management is critical for keyboard-only users.
Component level
You can create keyboard traps when a component's focus is not properly managed. A keyboard trap occurs when a keyboard-only user gets stuck in a component, or the focus is not maintained when it should be.
One of the most common patterns where users experience focus management issues is in a modal component. When a keyboard-only user encounters a modal, the user should be able to tab between the actionable elements of the modal, but they should never be allowed outside of the modal without explicitly dismissing it. JavaScript is essential to properly trapping this focus.
{% Compare 'worse' %}
{% Codepen { user: 'web-dev-codepen-external', id: 'ZERVJMz', height: 350, theme: 'auto', tab: 'js,result' } %}
{% endCompare %}
{% Compare 'better' %}
{% Codepen { user: 'web-dev-codepen-external', id: 'zYaydmv', height: 350, theme: 'auto', tab: 'js,result' } %}
{% endCompare %}
Page level
Focus must also be maintained when a user navigates from page-to-page. This is especially true in SPAs, where there is no browser refresh, and all content dynamically changes. Anytime a user clicks on a link to go to another page within your application, the focus is either kept in the same place or potentially placed somewhere else entirely.
When transitioning between pages (or routing), the development team must decide where the focus goes when the page loads.
There are multiple techniques to achieve this:
- Place focus on the main container with an
aria-liveannouncement. - Put the focus back to a link to skip to the main content.
- Move the focus to the top-level heading of the new page.
Where you decide to put the focus will depend on the framework you are using and the content you want to serve up to your users. It may be context- or action-dependent.
State management
Another area where JavaScript is critical to accessibility is state management, or when a component or page's current visual state is relayed to a low-vision, blind, or deafblind assistive technology user.
Often, the state of a component or page is managed through ARIA attributes, as introduced in the ARIA and HTML module. Let's review a few of the most common types of ARIA attributes used to help manage the state of an element.
Component level
Depending on your page content and what information your users need, there are many ARIA states to consider when relaying information about a component to the user.
For example, you may use an
aria-expanded
attribute to tell the user whether a drop-down menu or list is expanded or
collapsed.
{% Codepen { user: 'web-dev-codepen-external', id: 'oNyJeQZ', height: 300, theme: 'auto', tab: 'html,result' } %}
Or you might use aria-pressed
to indicate that a button has been pressed.
{% Codepen { user: 'web-dev-codepen-external', id: 'jOKXLyv', height: 300, theme: 'auto', tab: 'html,result' } %}
It's important to be selective when applying ARIA attributes. Think through the user flow to understand what critical information should be conveyed to the user.
Page level
Developers often use a visually hidden area called the ARIA live region to announce changes on the screen and alert messages to assistive technology (AT) users. This area can be paired with JavaScript to notify users of dynamic changes to the page without requiring the entire page to reload.
{% Codepen { user: 'web-dev-codepen-external', id: 'Yzvdxdr', height: 450, theme: 'auto', tab: 'result' } %}
Historically, JavaScript has struggled to announce content in
aria-live
and alert regions due to its dynamic nature. Asynchronously adding content into
the DOM makes it hard for AT to pick up the region and announce it.
For the content to be properly read, the live or alert region must be in the
DOM on load, then the text can dynamically be swapped out.
If you use a JavaScript framework, the good news is almost all of them have a "live announcer" package that does all the work for you and is fully accessible. There is no need to worry about creating a live region and dealing with the issues described in the previous section.
Here are some live packages for common JavaScript frameworks:
- React: react-aria-live and react-a11y-announcer
- Angular:
LiveAnnouncer - Vue: vue-a11y-utils
Modern JavaScript is a powerful language that allows web developers to create robust web applications. This sometimes leads to over-engineering and, by extension, inaccessible patterns. By following the JavaScript patterns and tips in this module, you can make your apps more accessible to all users.
{% Aside %} Special thanks to Mark Steadman for providing additional support on this module. Read more of his accessibility articles. {% endAside %}
{% Assessment 'javascript' %}
How is digital accessibility measured?
Digital accessibility means designing and building your digital offerings so that, regardless of a person's mental or physical ability, they can still interact with your website, app, or other digital product in a meaningful and equal way.
But how do you measure the accessibility of a digital product? How do you know when something is accessible?
Introduction to accessibility testing
There are many ways to test a digital product for accessibility. One fundamental approach is to evaluate it against a set of accessibility standards.
There are many types of accessibility standards. Typically, your industry, product type, local/country laws and policies, or overall accessibility goals will dictate which set of guidelines to follow and levels to meet. If no specific standard is required for your project, the standard recommendation is to follow the latest version of the Web Content Accessibility Guidelines (WCAG).
Testing your digital product against an accessibility standard and conformance level is commonly referred to as an accessibility audit. An accessibility audit uses various methodologies, techniques, and tools, including design, automated, manual, and assistive technology (AT) testing.
Perform an accessibility audit to capture the baseline accessibility compliance of a digital product. But, running it once at the start of a project is not enough to determine if a product is accessible. You should run this audit multiple times throughout the software product lifecycle to check for changes in the level of conformance, against a set of pre-determined accessibility checkpoints or guidelines.
Web Content Accessibility Guidelines (WCAG) {:#wcag}
The Web Content Accessibility Guidelines (WCAG) are an international set of accessibility standards developed through the W3C, in cooperation with individuals and organizations. The goal of WCAG is to provide a single shared standard for digital accessibility that meets the needs of individuals, organizations, and governments worldwide.
WCAG is primarily intended for web-based and native mobile app designers and developers. However, many others, including software developers, content creators/editors, and all levels of management, benefit from understanding and applying WCAG-based techniques to their process. Additional W3C standards may apply to your role, including the Authoring Tool Accessibility Guidelines (ATAG) and User Agent Accessibility Guidelines (UAAG), so make sure you review the W3C list of standards and use the one(s) most applicable to your role and project.
In terms of accessibility, WCAG is considered the "gold standard" for conformance testing. The first draft of WCAG was released in 1999. The current version is WCAG 2.1, released in June 2018, while WCAG 2.2 is scheduled for 2023. A completely revamped version of the guidelines, WCAG 3.0, is being drafted for a future release, but is not expected to be a completed W3C standard for a few more years.
The WCAG guidelines have three levels of success criteria: A, AA, and AAA. The success criteria determine conformance to WCAG. To meet WCAG conformance, the digital product you are testing needs to meet the success criteria for your target level.
30
A success criteria
20
AA success criteria
28
AAA success criteria
For the current standard (WCAG 2.1), there are 78 success criteria in total, split across each level. It is important to note that each level is progressive, meaning if your accessibility goal is AA, you must pass the success criteria for both A and AA to achieve this level of conformance.
30
Pass A level
50
Pass A + AA level
78
Pass A + AA + AAA level
Accessibility principles
The WCAG success criteria are a very important set of detailed guidelines that inform designers and developers how to create accessible websites and apps. Understanding these guidelines is critical to address issues that arise in accessibility compliance testing, but the guidelines quickly become very technical.
If you are new to this field, start with the principles of WCAG—Perceivable, Operable, Understandable, and Robust (POUR). By applying POUR principles to your digital products, you can focus on how your products are used by real humans, including people with disabilities.
{% Details 'open' %}
{% DetailsSummary 'h3' %} Perceivable {% endDetailsSummary %}
The first category in POUR is Perceivable. This principle states that users must be able to perceive all essential information on the screen, and it must be conveyed to multiple senses.
Ask yourself: Is there any content or functionality in your digital product that a person with a specific disability would not be able to perceive? Be sure to consider all the different types of disabilities—visual, mobility, hearing, cognitive, and speech impairments, vestibular and seizure disorders, and more.
Examples of Perceivable:
- Adding text alternatives to all non-decorative images and essential icons.
- Adding captions, transcripts, and audio descriptions to videos.
- Ensuring color is not the only method of conveying meaning. {% endDetails %}
{% Details 'open' %}
{% DetailsSummary 'h3' %} Operable {% endDetailsSummary %}
The second category is Operable. For this principle, users must be able to operate the digital product's interface. The interface cannot require interaction that a user cannot perform.
Ask yourself: Can users control the interactive elements of your digital product? Are there any focus order issues or keyboard traps? How are touch interfaces handled?
Examples of Operable:
- Adding keyboard and touchscreen support to all active elements.
- Ensuring slideshows and videos have all of the necessary controls available.
- Giving users enough time to fill out a form or a method to extend the time. {% endDetails %}
{% Details 'open' %}
{% DetailsSummary 'h3' %} Understandable {% endDetailsSummary %}
The third category of POUR is Understandable. For this principle, users must understand the information and the operation of the user interface.
Ask yourself: Is all of the content clearly written? Are all of the interactions easy to understand? Does the order of the page make sense—to sighted users, keyboard-only users, screen reader users?
Examples of Understandable:
- Writing simply—don't use a complex word when a simple one will do.
- Ensuring your digital product has predictable navigation.
- Ensuring error messages are clear and easy to resolve. {% endDetails %}
{% Details 'open' %}
{% DetailsSummary 'h3' %} Robust {% endDetailsSummary %}
The last category is Robust. This principle focuses on supporting assistive technologies and ensuring that, as devices and user agents evolve, the digital product remains accessible.
Ask yourself: What types of assistive technology are you supporting? Does your digital product only work on the newest browsers or operating systems? Does it work at all breakpoints and in different device orientations?
Examples of Robust:
- Testing keyboard-only navigation.
- Testing with different screen reader technologies.
- Ensuring all of the content and functionality can be accessed, regardless of device size or orientation. {% endDetails %}
Remember—the whole point of POUR is not about rigidly adhering to hard and fast rules. Instead, it is a way to help you understand and meet the diverse needs of your users.
{% Assessment 'measure' %}
The Document
Along with structure, there are many supporting HTML elements to consider when building and designing for digital accessibility. Throughout the Learn Accessibility course, we cover a lot of elements.
This module focuses on very specific elements that don't quite fit into any of the other modules but are useful to understand.
{% Aside %} Our Learn HTML course covers the basics of HTML and semantic structure in great detail. As such, this module builds off of that course material and is focused specifically on digital accessibility. Likewise, be sure to review the ARIA and HTML module in this course before diving into this module. {% endAside %}
Page title
The HTML <title>
element defines the content of the page or screen a user is about to
experience. It's found in the
<head> section of
an HTML document and is equivalent to the <h1> or main topic of the page. The
title content is displayed in the browser tab and helps users understand which
page they are visiting, but it is not displayed on the website or app itself.
In a single-page app (SPA),
the <title> is handled in a slightly different way, as users don't navigate
between pages as they do on multi-page websites. For SPAs, the value of the
document.title
property can be added manually or by a helper package, depending on the
JavaScript framework. Announcing the
updated page titles
to a screen reader user may take some additional work.
Descriptive page titles are good for both users and search engine optimization (SEO)—but don't go overboard and add lots of keywords. Since the title is the first thing announced when an AT user visits a page, it must be accurate, unique, and descriptive, but also concise.
When writing page titles, it is also best practice to "front load" the interior page or important content first, then add any preceding pages or information after. This way, AT users don't have to sit through the information they have already heard.
{% Compare 'better' %}
<title>Season 3 | Outrageous Pumpkins | The Food Channel</title>
{% endCompare %}
{% Aside 'important' %} Search engines typically display only the first 55–60 characters of a page title, so be sure to limit your total page title characters. {% endAside %}
Language
Page language
The page language attribute (lang) sets the default language for the entire page. This attribute is added to the <html> tag. A valid language attribute should be added to every page as it signals the AT to which language it should use.
It's recommended that you use two-character ISO language codes for greater AT coverage, as many of them do not support extended language codes.
When a language attribute is completely missing, the AT will default to the user's programmed language. For example, if an AT was set to Spanish, but a user visited an English website or app, the AT would try to read the English text with Spanish accents and cadence. This combination results in an unusable digital product and a frustrated user.
{% Compare 'better' %}
<html lang="en">...</html>
{% endCompare %}
The lang attribute can only have one language associated with it. This means
the <html> attribute can only have one language, even if there are multiple
languages on the page. Set lang to the primary language of the page.
{% CompareCaption %} Multiple languages are not supported. {% endCompareCaption %} {% endCompare %}
{% Compare 'better' %}
<html lang="ar">...</html>
{% CompareCaption %} Set only the page's primary language. In this case, the language is Arabic. {% endCompareCaption %} {% endCompare %}
Section language
You can also use the language attribute (lang) for language switches in the content itself. The same basic rules apply as the full-page language attribute, except you add it to the appropriate in-page element instead of on the <html> tag.
Remember that the language you add to the <html> element cascades down to all
the contained elements, so always set the primary language of the page
top-level lang attribute first.
For any in-page elements written in a different language, add that lang
attribute to the appropriate wrapper element. This will override the
top-level language setting until that element is closed.
While traveling in Estonia this summer, I often asked, "Kas sa räägid inglise keelt?" when I met someone new.
{% Compare 'better' %}
<html lang="en">
<body>...
<div>
<p>While traveling in Estonia this summer, I often asked,
<span lang="et">"Kas sa räägid inglise keelt?"</span>
when I met someone new.</p>
</div>
</body>
</html>
{% endCompare %}
iFrames
The iFrame element
(<iframe>) is
used to host another HTML page or a third party's content within the page. It
essentially puts another webpage within the parent page. iFrames are commonly
used for advertisements, embedded videos, web analytics, and interactive
content.
To make your <iframe> accessible, there are a couple of aspects to consider. First, each <iframe> with distinct content should include a title element inside the parent tag. This title supplies AT users with more information about the content inside the <iframe>.
Second, as a best practice, it is good to set the scrolling to "auto" or "yes" in the <iframe> tag settings. This allows people with low vision to be able to scroll into content within the <iframe> that they might not otherwise be able to see. Ideally, the <iframe> container would also be flexible in its height and width.
{% Compare 'better' %}
<iframe title="Google Pixel - Lizzo in Real Tone"
src="https://www.youtube.com/embed/3obixhGZ5ds"
scrolling="auto">
</iframe>
{% endCompare %}
{% Assessment 'document' %}
Animation and motion
Have you ever been riding in a car, boat, or plane and suddenly felt the world spin? Or had a migraine so bad that animations on your phone or tablet, created to "delight" you, suddenly make you sick? Or perhaps you've always been sensitive to all types of motion? These are examples of different types of vestibular disorders.
By age 40, over 35% of adults will have experienced some form of vestibular dysfunction. This may lead to a temporary dizzy spell, migraine-induced vertigo, or a more permanent vestibular disability.
Beyond triggering vertigo, many people find moving, blinking, or scrolling content distracting. People with ADHD and other attention deficit disorders might be so distracted by your animated elements that they forget why they even went to your website or app in the first place.
In this module, we'll look at some of the ways to help better support people with all types of movement-triggered disorders.
Flashing and moving content
When building animation and motion, you should ask yourself whether the element's movement is excessive. For example, colors flickering from dark to light or quick movements on the screen, can cause seizures in people with photosensitive epilepsy. It is estimated that 3% of people with epilepsy suffer from photosensitivity, and it's more common in women and younger people.
The WCAG's guidelines on flashing recommend against the following:
- Flashes for more than three times in any one second
- Flashes below the general flash and red flash threshold.
These flashes may, at best, cause an inability to use a webpage or, at worst, lead to illness.
For any extreme movement, it is imperative that you test it using the Photosensitive Epilepsy Analysis Tool (PEAT). PEAT is a free tool to identify if the screen's content, video, or animations are likely to cause seizures. Not all content needs to be evaluated by PEAT, but content that contains flashing or rapid transitions between light and dark background colors should be evaluated, just to be safe.
Another question you should ask yourself about animation and motion is whether the element's movement is essential to understanding the content or actions on the screen. If it is not essential, consider removing all movement—even micro-movements—from the element you are building or designing.
Suppose you believe the element's movement is not essential but could enhance the user's overall experience, or you cannot remove the movement for another reason. In that case, you should follow WCAG's guidelines on motion. The guidelines state that you must build an option for users to pause, stop or hide movement for: non-essential moving, blinking, or scrolling elements that start automatically, last more than five seconds, and are part of other page elements.
Pause, stop, or hide movement {: #pause-stop-hide}
Add a pause, stop, or hide mechanism to your page to allows users to turn off potentially problematic motion animation. You can do this on the screen level or element level.
For example, suppose your digital product includes a lot of animations. Consider adding an accessible JavaScript toggle to allow users to control their experience. When the button is toggled to "motion off," all animations are frozen on that screen and all others.
{% Codepen { user: 'web-dev-codepen-external', id: 'xxjpBGE', height: 450, theme: 'auto', tab: 'js,result' } %}
Use media queries
In addition to being selective about your animations, giving your users options to pause, stop, hide movement, and avoiding infinite animation loops, you can also consider adding a movement-focused media query. This gives your users even more choice when it comes to what is displayed on the screen.
@prefers-reduced-motion {: #prefers-reduced-motion}
Similar to the color-focused media queries in the color module, the @prefers-reduced-motion media query checks the user's OS settings related to animation.
A user may set display preferences to reduce motion. These settings are different across operating systems, and may be framed positively or negatively. With @prefers-reduced-motion, you can design a site which respects these preferences.
{% BrowserCompat 'css.at-rules.media.prefers-reduced-motion' %}
On MacOS and Android, the user turns the setting on to reduce motion. On MacOS, a user can set Reduce motion in Settings > Accessibility > Display. Android's setting is Remove animations. On Windows, the setting is framed positively as Show animations, which is on by default. A user must turn this setting off to reduce motion.
{% Aside %} As each UI uses different language, consider still allowing users to choose for themselves, rather than guessing how much animation is too much. This may differ on a situation-by-situation basis. {% endAside %}
{% Codepen { user: 'web-dev-codepen-external', id: 'VwxyReM', height: 350, theme: 'auto', tab: 'css,result' } %}
Alternatively, as shown in the next set of examples, you can code all your animations to stop moving within five seconds or less instead of playing on an infinite loop.
Progressive enhancement for movement
As designers and developers, we have a lot of choices to make, including default movement states and how much movement to display. Let's take another look at the last example on motion.
Suppose we decide the animation is unnecessary for understanding the content on the screen. In that case, we can choose to set the default state to the reduced motion animation instead of the full motion version. Unless users specifically ask for animations, the animations are turned off.
{% Aside %} The motion preferences available are dependent on the operating system. Some allow you to opt-in to animations, while others require you to opt out of the animations. {% endAside %}
We cannot predict what level of movement will cause issues for people with seizure, vestibular, and other visual disorders. Even a small amount of motion on the screen can trigger dizziness, blurred vision, or worse. So, in the following example, we default to no animation.
{% Codepen { user: 'web-dev-codepen-external', id: 'qBYpvam', height: 350, theme: 'auto', tab: 'css,result' } %}
Layered media queries
You can use multiple media queries to give your users even more choices. Let's
use @prefers-color-scheme, @prefers-contrast, and @prefers-reduced-motion
all together.
{% Codepen { user: 'web-dev-codepen-external', id: 'wvjpOzR', height: 400, theme: 'auto', tab: 'css,result' } %}
Allow your users to choose {: #allow-user-choice}
While it can be fun to build animation into our digital products to delight users, it's critical we remember some people will be affected by these designs. Motion sensitivity can affect anyone, from feeling slight discomfort to causing a debilitating illness or seizure.
You can use a number of different tools to allow the user to decide what's best for them, rather than guessing how much motion is too much. For example, add a toggle to turn on or off animation within your site or web app. Consider defaulting such a toggle to off.
{% Assessment 'motion' %}
Patterns, components, and design systems
Many people use component-driven development using pattern style guides, component libraries, or full design systems in their development workflow process. Even if you have not used these tools formally, it's likely you use a similar process to break up a large design for a website, app, or other digital product into manageable pieces.
Like building a physical structure, it's important to build one piece at a time. First, the foundation, the structure, walls, windows, roof, and everything in between. Component-driven development tools allow us to do this for websites, apps, and other digital products.
Some perks to component-driven development include breaking things down into manageable pieces, so there is less development time with these reusable components. It allows designers, frontend and backend developers, and QA to work simultaneously. And clients, designers, PMs, and more, like it because they can preview the build process and use a living style guide as a reference after the website has launched.
However, when we look at patterns, components, and design systems with accessibility in mind, some questions arise. How do you know which patterns are best when it comes to accessibility? Should you use an established pattern/library or create new ones? How do you know if these patterns will actually help your users?
With the myriad of choices available, it's easy to quickly become confused about this topic. This module aims to give you general information on how to evaluate patterns, components, and design systems for accessibility and gives you a starting point to help you make more accessible choices.
Think critically
Choosing an accessible pattern, component, or design system is not rocket science, but it does take time and critical thinking. In fact, there's no such thing as "one perfect pattern," but there may potentially be many options that could work. It's about learning to choose the best option for your unique situation.
In the subsequent testing modules, you will learn more about the techniques and methods on how to evaluate patterns, components, and design systems for accessibility. But before that stage, you need to step back and ask yourself some fundamental questions, such as:
- Does an established accessible pattern, component, or design system already exist?
- What browsers and assistive technology (AT) am I supporting?
- Are there any code/framework limitations or other integrations/factors/user needs I need to consider?
Depending on your dev environment and user needs, you may have additional or different questions from these. Consider these questions as your starting point in your accessibility evaluation.
Established resources
Before building something brand new, do your due diligence and review what already exists in terms of accessible patterns, components, and design systems. With just a little research, you might be surprised to find a solution—or multiple—that fits your needs.
Some great resources for accessible patterns, components, and design systems include:
- Accessible Components
- Deque University ARIA library
- Gov.UK Design System
- Inclusive Components
- MagentaA11y
- U.S. Web Design System (USWDS), created for the U.S. federal government
- List of accessible patterns from Smashing Magazine
For JavaScript frameworks, the following resources are fairly accessible out of the box or are easy to customize for accessibility:
- When CSS Isn't Enough: JavaScript Requirements For Accessible Components
- React
- Angular: Material library
- Vue: Vuetensils
However—and this cannot be stressed enough—you should never just copy/paste code and assume it will fit within your environment and automatically meet your user needs. This is true of all patterns, components, and design systems, even if labeled as fully accessible.
All resources should be seen as a starting point. Be sure to test everything!
Browsers and assistive technology (AT) support
After researching a few base patterns, components, or a full design system that might work for your dev environment, you can move on to assistive technology support. One major type of AT you will want to focus on when evaluating patterns, components, and design systems is screen readers.
Screen readers were built with specific browsers in mind and will work best when paired with these browsers. We'll go into this topic in much more detail in the module on AT testing, but for pattern evaluation purposes, it is helpful to understand these combinations exist, so you know what you need in terms of support.
| Screen reader | OS | Browser compatibility | Cost |
|---|---|---|---|
| Job Access with Speech (JAWS) | Windows | Chrome, Firefox, Edge | License require (a free 40-min version exists) |
| Non-Visual Desktop Access (NVDA) | Windows | Chrome and Firefox | Free (requires download) |
| Narrator | Windows | Edge | Free (built into Windows machines) |
| VoiceOver | macOS | Safari | Free (built into macOS machines) |
| Orca | Linus | Firefox | Free (built into Gnome-based distributions) |
| TalkBack | Android | Chrome and Firefox | Free (built into certain versions of Android OS) |
| VoiceOver | iOS | Safari | Free (built into iOS devices) |
Browser support is generally complicated, and things get even trickier when you add AT devices and ARIA specifications to the mix.
But it's not all bad news. Thankfully, great resources such as HTML5 Accessibility, Accessibility Support, and WCAG's Custom Control Accessible Development Checklist help us to better understand current browser and AT device support, and even when to use ARIA in the first place.
These resources outline the different HTML and ARIA pattern sub-elements available, including open-source community tests. You can also review some pattern examples—for both desktop and mobile browsers/AT devices. As such, these resources can help you make a more accessible decision regarding patterns, components, and design systems that you may want to use.
Other considerations
Once you have chosen a few accessible base patterns or components, and factored in browser and AT device support, you can move on to more specific contextual questions about the pattern, component, design system, and dev environment.
For example, if you are working in a management system (CMS) or have legacy code, there may be some limitations to which patterns you can use. Upon review, several pattern choices may quickly be cut to one or two options.
Many JavaScript frameworks allow developers to use almost any pattern they choose. In cases like these, you may have fewer restrictions and can choose the most accessible pattern option.
There are additional considerations to weigh when choosing a pattern, component, or design system, such as:
- Performance
- Security
- Search engine optimization
- Language translation support
- Third-party integrations
These factors will undoubtedly play into your pattern choice, but you should also consider the people creating the content and code itself. The pattern you choose must be robust enough to handle any potential limitations around editor-generated or user-generated content, plus be built in a way that developers of all accessibility knowledge can use.
{% Assessment 'patterns' %}
Content structure
One of the most important aspects of digital accessibility is the underlying structure of the page. When you build your website or app using structural elements instead of relying on styles alone, you give critical context to people using assistive technologies (AT), such as screen readers.
Structural elements serve as an outline of the content on the screen, but they also act as anchor points to allow for easier navigation within the content.
When you use semantic HTML elements, the inherent meaning of each element is passed on to the accessibility tree and used by the AT, giving more meaning to the content than non-semantic elements. There may be cases where you need to add additional ARIA attributes to build relationships or to enhance the overall user experience, but in most situations, one of the 100+ HTML elements available should work fairly well on its own.
While this module focuses on the most widely used structural elements critical to digital accessibility, there are certainly additional elements and environmental factors to consider when building structure into your website or app. These examples are an introduction to the topic, rather than all-inclusive.
{% Aside %}
Our Learn HTML course covers the basics of HTML and semantic structure in great detail. As such, this module builds off of that course material and is focused specifically on digital accessibility. Likewise, be sure to review the ARIA and HTML module in this course before diving into this module.
{% endAside %}
Landmarks
Users of AT rely on structural elements to give them information about the page's overall layout. When sectioning off large regions of content, you can use either ARIA landmark roles or the newer HTML landmark elements to add structural context to your page.
Landmarks ensure content is in navigable regions. It's recommended that you supply at least one landmark per page.
Some resources suggest combining ARIA and HTML landmarks together to provide better AT coverage. While this type of redundancy shouldn't cause issues for your users, test the patterns using a screen reader to be certain. When in doubt, it's best to default to using only the newer HTML landmark elements, as the browser support is very robust.
Let's compare the HTML landmark elements as mapped to their counterpart ARIA landmark roles.
| HTML landmark element | ARIA landmark role |
|---|---|
<header> |
banner |
<aside> |
complementary |
<footer> |
contentinfo |
<nav>
|
navigation |
<main> |
main |
<form> 1 |
form |
<section> 1 |
region |
Now, let's compare examples of accessible content structure.
Stamp collecting, also known as philately, is the study of postage stamps, stamped envelopes, postmarks, postcards, and other materials relating to postal delivery.
© 2022 - Stamps R Awesome
{% Compare 'better' %}
<header>
<nav>...</nav>
</header>
<main>
<section aria-label="Introduction to stamp collecting">
<p>Stamp collecting, also known as philately, is
the study of postage stamps, stamped envelopes,
postmarks, postcards, and other materials relating
to postal delivery.</p>
</section>
</main>
<footer>
<p>© 2022 - Stamps R Awesome</p>
</footer>
{% endCompare %}
{% Aside %}
Check out the ARIA Landmarks Example for more information and best practices.
{% endAside %}
Headings
When implemented correctly, HTML heading levels form a succinct outline of the overall page content.
There are six heading levels we can use. Heading level one <h1> is used for the page's highest and most important information, moving incrementally to
heading level six <h6> for the lowest and least important information.
The sequence of the heading levels is important. Ideally, you won't skip
heading levels, for example, starting a section with an <h1> and immediately
following it with an <h5>. Instead, you should progress to the <h5> in
order. Heading level order is especially important to AT users
as this is one of their primary ways to navigate through content.
{% Aside %}
While you can use ARIA heading roles for heading levels, it's recommended you use semantic HTML heading levels whenever possible.
{% endAside %}
To help you adhere to the proper semantic structure and order for headings, consider decoupling your CSS styles from the heading levels. This allows you more flexibility in heading styles while maintaining the heading level order. It's critical you don't use styles alone to create headings, as these aren't seen by AT as a heading.
{% Codepen { user: 'web-dev-codepen-external', id: 'Vwdajpe', height: 400, theme: 'auto', tab: 'html,css,result' } %}
When we fake headings, the appropriate structure isn't conveyed to AT users.
Headings are also helpful for people with cognitive and attention deficit disorders. It's important to make the heading content meaningful to help them understand what is most important on the page.
Stamps
Stamp collecting, also known as philately, is the study of postage stamps, stamped envelopes, postmarks, postcards, and other materials relating to postal delivery.
How do I start a stamp collection?
Equipment you will need
...
How to acquire stamps
...
Organizations you can join
...
{% Compare 'better' %}
<header>
<h1>Stamp collecting</h1>
</header>
<main>
<section aria-label="Introduction to stamp collecting">
<h2>What is stamp collecting?</h2>
<p>Stamp collecting, also known as philately, is the study of postage stamps, stamped envelopes, postmarks, postcards, and other materials relating to postal delivery.</p>
</section>
<section aria-label="Start a stamp collection">
<h2>How do I start a stamp collection?</h2>
<h3>Required equiment</h3>
<p>...</p>
<h3>How to acquire stamps</h3>
<p>...</p>
<h3>Organizations you can join</h3>
<p>...</p>
</section>
</main>
{% endCompare %}
Lists
HTML lists are a way to semantically group items similar to one other giving them inherent meaning, much like your grocery store list or that never-ending to-do list you keep ignoring.
There are three types of HTML lists:
The list item <li>
element is used to represent an item in an ordered or unordered list, while the
description term
<dt> and definition
<dd> elements can
be found in the description list.
When programmed correctly, these elements can inform non-sighted AT users about the visible structure of the list. When an AT encounters a semantic list, it can tell the user the list name and how many items are in it. As the user navigates within the list, the AT will read each list item out loud and tell which number it's in the list—item one of five, item two of five, and so on.
Grouping items into lists also helps sighted people who have cognitive and attention disorders and those with reading disabilities, as list content is typically styled to have more visual whitespace and the content is to the point.
How do I start a stamp collection?
Equipment you will need
Small tongs with rounded tips
Stamp hinges
Stockbook or albumn
Magnifying glass
Stamps
{% Compare 'better' %}
<div>
<h1>How do I start a stamp collection?</h1>
<h2>Equipment you will need</h2>
<ol>
<li>Small tongs with rounded tips</li>
<li>Stamp hinges</li>
<li>Stockbook or albumn</li>
<li>Magnifying glass</li>
<li>Stamps</li>
</ol>
</div>
{% endCompare %}
Tables
In HTML, tables are generally used for organizing data or page layout.
Depending on the table's purpose, you'll use different semantic structural elements. Tables can be very complex in structure, but when you stick to the basic semantic rules, they are fairly accessible without much intervention.
Layout
In the early days of the internet, layout tables were the primary HTML element
used to build visual structures. Today, we use a mix of semantic and
non-semantic elements such as <div>s and landmarks to create layouts.
While the days of creating layout tables are mostly over, there are times when
layout tables are still used, such as in visually rich emails, newsletters, and
advertisements. In these cases, tables used only for layout must not
use structural elements that convey relationships and add context, such as
<th> or <caption>.
Layout tables must also be hidden from AT users with the ARIA presentation role or aria-hidden state.
| [Stamp Image 1] | [Stamp Image 2] | [Stamp Image 3] |
|---|
{% Compare 'better' %}
<table role="presentation">
<tr>
<td>[Stamp Image 1]</td>
<td>[Stamp Image 2]</td>
<td>[Stamp Image 3]</td>
</tr>
</table>
{% endCompare %}
Data
Unlike a layout table, which should be hidden from AT users, a data table and all its elements must be exposed. The structure of data tables is critical for conveying simple and complex information to users.
When a table is structured correctly, relationships form between the column headers and rows and the data within the table. When structured incorrectly, the user is left to decipher the meaning and context of the information in the table.
Depending on the complexity of the table, forming relationships through code is
accomplished in different ways. The first step to making a table accessible is
to mark up header cells with
<th> and data cells
with <td> elements.
For more complex tables, you may need to use additional HTML table elements
such as
<rowgroup>,
<colgroup>,
<caption>,
and
scope to
convey meaning and relationships.
| Animal | Year | Condition |
| Bird | 1947 | Fair |
| Lion | 1982 | Good |
| Horse | 1978 | Mint |
{% Compare 'better' %}
<table>
<caption>My stamp collection</caption>
<tr>
<th>Animal</th>
<th>Year</th>
<th>Condition</th>
</tr>
<tr>
<td>Bird</td>
<td>1947</td>
<td>Fair</td>
</tr>
<tr>
<td>Lion</td>
<td>1982</td>
<td>Good</td>
</tr>
<tr>
<td>Horse</td>
<td>1978</td>
<td>Mint</td>
</tr>
</table>
{% endCompare %}
Assistive Technology testing
{% Aside %} This module is a continuation of the previous two testing modules, automated accessibility testing and manual accessibility testing. If you have not gone through the exercises in those modules yet, we encourage you to do so, as this module starts where they left off. {% endAside %}
This module focuses on using assistive technology (AT) for accessibility testing. A person with disabilities can use AT to help increase, maintain, or improve the capabilities of performing a task.
In the digital space, ATs can be:
- No/Low-tech: head/mouth sticks, hand-held magnifiers, devices with large buttons
- High-tech: voice-activated devices, eye-tracking devices, adaptive keyboards/mice
- Hardware: switch buttons, ergonomic keyboards, auto-refreshing Braille device
- Software: text-to-speech programs, live captions, screen readers
We encourage you to use multiple types of ATs in your overall testing workflow.
Screen reader testing basics
In this module, we focus on one of the most popular digital ATs, screen readers. A screen reader is a piece of software that reads the underlying code of a website or app. It then converts that information into speech or Braille output for the user.
Screen readers are essential for people who are blind and deafblind, but they also could benefit people with low vision, reading disorders, or cognitive disabilities.
Browser compatibility
There are multiple screen reader options available. The most popular screen readers today are JAWS, NVDA, and VoiceOver for desktop computers and VoiceOver and Talkback for mobile devices.
Depending on your operating system (OS), favorite browser, and the device that you use, one screen reader may stand out as the best option. Most screen readers are built with specific hardware and web browsers in mind. When you use a screen reader with a browser it was not calibrated for, you may encounter more "bugs" or unexpected behavior. Screen readers work best when used in the following combinations.
Screen reader commands
Once you have the proper set-up for your screen reader software for your desktop or mobile device, you should look at the screen reader documentation (linked in the preceding table) and run through some essential screen reader commands to familiarize yourself with the technology. If you have used a screen reader before, consider trying out a new one!
When using a screen reader for accessibility testing, your goal is to detect problems in your code that interfere with the usage of your website or app, not to emulate the experience of a screen reader user. As such, there is a lot you can do with some foundational knowledge, a few screen reader commands, and a bit—or a lot—of practice.
If you need to further understand the user experience of people using screen readers and other ATs, you can engage with many organizations and individuals to gain this valuable insight. Remember that using an AT to test code against a set of rules and asking users about their experience often yields different results. Both are important aspects to create fully inclusive products.
Key commands for desktop screen readers
Key commands for mobile screen readers
Screen reader testing demo
To test our demo, we used a Safari on a laptop running MacOS and capture sound. You can walk through these steps using any screen reader, but the way you encounter some errors may be different from how its described in this module.
Step 1
Visit the updated CodePen, which has all the automated and manual accessibility updates applied.
View it in debug mode to proceed with the
next tests. This is important, as it removes the <iframe> which surrounds the
demo webpage, which may interfere with some testing tools. Learn more about
CodePen's debug mode.
Step 2
Activate the screen reader of your choice and go to the demo page. You may consider navigating through the entire page from top to bottom before focusing on specific issues.
We've recorded the our screen reader for each issue, before and after the fixes are applied to the demo. We encourage you to run through the demo with your own screen reader.
Issue 1: Content structure {: #content-structure}
Headings and landmarks are one of the primary ways people navigate using screen readers. If these are not present, a screen reader user has to read the entire page to understand the context. This can take a lot of time and cause frustration. If you try to navigate by either element in the demo, you will quickly discover that they do not exist.
- Landmark example:
<div class="main">...</div> - Heading example:
<p class="h1">Join the Club</p>
If you have updated everything correctly, there should not be any visual changes, but your screen reader experience will have dramatically improved.
Some inaccessible elements can't be observed by just looking at the site. You may remember the importance of heading levels and semantic HTML from the Content structure module. A piece of content may look like a heading, but the content is actually wrapped in a stylized <div>.
To fix the issue with headings and landmarks, you must first identify each element that should be marked up as such and update the related HTML. Be sure to update the related CSS as well.
Landmark example: <main>...</main>
Heading example: <h1>Join the Club</h1>
If you have updated everything correctly, there should not be any visual changes, but your screen reader experience will have dramatically improved.
Issue 2: Link context {: #link-context}
It's important to give content to screen reader users about the purpose of a link and if the link is redirecting them to a new location outside of the website or app.
In our demo, we fixed most of the links when we updated the active image alternative text, but there are a few additional links about the various rare diseases that could benefit from additional context—especially since they redirect to a new location.
<a href="https://rarediseases.org/rare-diseases/maple-syrup-urine-disease">
Maple syrup urine disease (MSUD)
</a>
To fix this issue for screen reader users, we update the code to add more information, without affecting the visuals element. Or, to help even more people such as those with reading and cognitive disorders, we may choose to add additional visual text instead.
There are many different patterns we may consider to add additional link information. Based on our simple environment that supports just one language, an ARIA label is a straightforward option in this situation. You may notice that the ARIA label overrides the original link text, so make sure to include that information in your update.
<a href="https://rarediseases.org/rare-diseases/maple-syrup-urine-disease"
aria-label="Learn more about Maple syrup urine disease on the Rare Diseases website.">
Maple syrup urine disease (MSUD)
</a>
Issue 3: Decorative image
In our automated testing module, Lighthouse was unable to pick up on the inline SVG that acts as the main splash image on our demo page—but the screen reader finds it and announces it as "image" without additional information. This is true, even without explicitly adding the role="img" attribute to the SVG.
<div class="section-right">
<svg>...</svg>
</div>
To fix this issue, we first need to decide if the image is informative or decorative. Based on that decision, we need to add the appropriate image alternative text (informative image) or hide the image from screen reader users (decorative).
We weighed the pros and cons of how best to categorize the image and decided it was decorative, which means we want to add or modify the code to hide the image. A quick method is to add a role="presentation" to the SVG image directly. This sends a signal to the screen reader to skip over this image and not list it in the images group.
<div class="section-right">
<svg role="presentation">...</svg>
</div>
Issue 4: Bullet decoration {: #bullet-decoration}
You may have noticed that the screen reader reads the CSS bullet image under the rare diseases sections. While not the traditional type of image we discussed in the Images module, the image still must be modified as it disrupts the flow of the content and could distract or confuse a screen reader user.
<p class="bullet">...</p>
Much like the decorative image example discussed earlier, you can add a role="presentation" to the HTML with the bullet class to hide it from the screen reader. Similarly, a role="none" would work. Just be sure not to use aria-hidden: true or you will hide all of the paragraph information from screen reader users.
<p class="bullet" role="none">...</p>
Issue 5: Form field
In the Forms module, we learned that all form fields must also have a visual and programmatic label. This label must remain visible at all times.
In our demo, we're missing both a visual and programmatic label on our newsletter sign-up email field. There is a text placeholder element, but this does not replace the label as it's not visually persistent and is not fully compatible with all screen readers.
<form>
<div class="form-group">
<input type="email" placeholder="Enter your e-mail address" required>
<button type="submit">Subscribe</button>
</div>
</form>
To fix this issue, replace the text placeholder with a look-alike label element. That label element is programmatically connected to the form field and movement was added with JavaScript to keep the label visible even when content is added to the field.
<form>
<div class="form-group">
<input type="email" required id="youremail" name="youremail" type="text">
<label for="youremail">Enter your e-mail address</label>
<button type="submit" aria-label="Subscribe to our newsletter">Subscribe</button>
</div>
</form>
Wrap up
Congratulations! You have completed all of the testing for this demo. You can look at all of these changes in the updated Codepen for this demo.
Now, you can use what you've learned to review the accessibility of your own websites and apps.
The goal of all of this accessibility testing is to address as many possible issues that a user may potentially encounter. However, this does not mean that your website or app will be perfectly accessible when you're finished. You'll find the most success by designing your website or app with accessibility throughout the process, and incorporating these tests with your other pre-launch testing.
{% Assessment 'at' %}
Automated accessibility testing
So far in this course, you have learned about the individual, business, and legal aspects of digital accessibility, and the basics of digital accessibility conformance. You have explored specific topics related to inclusive design and coding, including when to use ARIA versus HTML, how to measure color contrast, when JavaScript is essential, amongst other topics.
In the remaining modules, we shift gears from designing and building to testing for accessibility. We'll utilize a three-step testing process that includes automated, manual, and assistive technology testing tools and techniques. We'll use the same demo throughout these testing modules to progress the web page from inaccessible to accessible.
Each test—automated, manual, and assistive tech—is critical to achieving the most accessible product possible.
Our tests rely on the Web Content Accessibility Guidelines (WCAG) 2.1 conformance level A and AA as our standards. Remember that your industry, product type, local/country laws and policies, or overall accessibility goals will dictate which guidelines to follow and levels to meet. If you don't require a specific standard for your project, the recommendation is to follow the latest version of WCAG. Refer back to "How is digital accessibility measured?" for general information on accessibility audits, conformance types/levels, WCAG, and POUR.
As you now know, accessibility conformance is not the full story when it comes to supporting people with disabilities. But, it's a good starting point as it provides a metric you can test against. We encourage you to take additional actions outside of accessibility conformance testing, such as running usability tests with people with disabilities, hiring people with disabilities to work on your team, or consulting an individual or company with digital accessibility expertise to help you build more inclusive products.
Automated testing basics
Automated accessibility testing uses software to scan your digital product for accessibility issues against pre-defined accessibility conformance standards.
Pros of automated accessibility tests:
- Easy to repeat tests at different stages of the product lifecycle
- Just a few steps to run and very quick results
- Little accessibility knowledge is required to run the tests or understand the results
Cons of automated accessibility tests:
- Automated tools don't catch all of the accessibility errors in your product
- Reported false positives (an issue is reported that isn't a true WCAG violation)
- Multiple tools may be needed for different product types and roles
Automated testing is a great first step to check your website or app for accessibility, but not all checks can be automated. We'll go into more detail on how to check the accessibility of elements that cannot be automated in the manual accessibility testing module.
Types of automated tools
One of the first online automated accessibility testing tools was developed in 1996 by the Center for Applied Special Technology (CAST), called "The Bobby Report." Today, there are over 100 automated testing tools to choose from!
Automated tool implementation varies from accessibility browser extensions to code linters, desktop and mobile applications, online dashboards, and even open-source APIs you can use to build your own automated tooling.
Which automated tool you decide to use can depend on many factors, including:
- Which conformance standards and levels are you testing against? This may include WCAG 2.1, WCAG 2.0, U.S. Section 508, or a modified list of accessibility rules.
- What type of digital product are you testing? This could be a website, web app, native mobile app, PDF, kiosk, or other product.
- What part of the software development life cycle are you testing your product?
- How much time does it take to set up and use the tool? For an individual, team, or company?
- Who is conducting the test: designers, developers, QA, etc.?
- How often do you want the accessibility to be checked? What details should be included in the report? Should issues be directly linked to a ticketing system?
- Which tools work best in your environment? For your team?
There are many additional factors to consider as well. Check out WAI's article on "Selecting Web Accessibility Evaluation Tools" for more information on how to select the best tool for you and your team.
Demo: Automated test
For the automated accessibility testing demo, we'll be using Chrome's Lighthouse. Lighthouse is an open-source, automated tool created to improve the quality of web pages through different types of audits, such as performance, SEO, and accessibility.
Our demo is a website built for a made-up organization, the Medical Mysteries Club. This site is intentionally made inaccessible for the demo. Some of this inaccessibility may be visible to you, and some (but not all) will be caught in our automated test.
Step 1
Using your Chrome browser, install the Lighthouse extension.
There are many ways to integrate Lighthouse into your testing workflow. We'll use the Chrome extension for this demo.
Step 2
We have built a demo in CodePen.
View it in debug mode to proceed with the
next tests. This is important, as it removes the <iframe> which surrounds the
demo web page, which may interfere with some testing tools. Learn more about
CodePen's debug mode.
Step 3
Open Chrome DevTools and navigate to the Lighthouse tab. Uncheck all of the category options except for "Accessibility." Keep the mode as the default and choose the device type you're running the tests on.
Step 4
Click the "Analyze page load" button and give Lighthouse time to run its tests.
Once the tests are complete, Lighthouse displays a score that measures how accessible the product you're testing is. The Lighthouse score is calculated by the number of issues, issue types, and the impact on users of the issues detected.
Beyond a score, the Lighthouse report includes detailed information about what issues it has detected and links to resources to learn more about remedying them. The report also includes tests that are passed or not applicable and a list of additional items to check manually.
{% Aside %}
The automated Lighthouse tests were run in December 2022. Due to changes in the codebase, browsers, assistive technology, accessibility standards, and/or rulesets, your test results may vary.
{% endAside %}
Step 5
Now, let's go through an example of each automated accessibility issue discovered and fix the relevant styles and markup.
Issue 1: ARIA roles {: #aria-roles}
The first issue states: "Elements with an ARIA [role] that require children to contain a specific [role] are missing some or all of those required children. Some ARIA parent roles must contain specific child roles to perform their intended accessibility functions." Learn more about ARIA role rules.
In our demo, the newsletter subscribe button fails:
<button role="list" type="submit" tabindex="1">Subscribe</button>
The "subscribe" button next to the input field has an incorrect ARIA role applied to it. In this case, the role can be removed completely.
<button type="submit" tabindex="1">Subscribe</button>
Issue 2: ARIA hidden {: #aria-hidden}
<input type="email" placeholder="Enter your e-mail address" aria-hidden="true" tabindex="-1" required>
The input field had an aria-hidden="true" attribute applied to it. Adding
this attribute hides the element (and everything nested under it) from
assistive tech.
<input type="email" placeholder="Enter your e-mail address" tabindex="-1" required>
In this case, you should remove this attribute from the input to allow people using assistive technology to access and enter information into the form field.
Issue 3: Button name {: #button-name}
<button role="list" type="submit" tabindex="1">Subscribe</button>
When you remove the inaccurate ARIA role from the button element in issue 1, the word "Subscribe" becomes the accessible button name. This functionality is built into the semantic HTML button element. There are additional pattern options to consider for more complex situations.
<button type="submit" tabindex="1">Subscribe</button>
Issue 4: Image alt attributes {: #image-alt-attributes}
Image elements are missing [alt] attributes. Informative elements should aim
for short, descriptive alternate text. Decorative elements can be ignored with
an empty alt attribute. Learn more about image alternative text
rules.
<a href="index.html">
<img src="https://upload.wikimedia.org/wikipedia/commons/….png">
</a>
Since the logo image is also a link, you know from the image module that it is called an actionable image and requires alternative text information about the purpose of the image. Normally, the first image on the page is a logo, so you can reasonably assume your AT users will know this, and you may decide not to add this additional contextual information to your image description.
<a href="index.html">
<img src="https://upload.wikimedia.org/wikipedia/commons/….png"
alt="Go to the home page.">
</a>
Issue 5: Link text {: #link-text}
Links do not have a discernible name. Link text (and alternate text for images, when used as links) that is discernible, unique, and focusable improves the navigation experience for screen reader users. Learn more about link text rules.
<a href="#!"><svg><path>...</path></svg></a>
All of the actionable images on the page must include information about where
the link will send users. One method to remedy this issue is to add alternative
text to the image about the purpose, as you did on the logo image in the
example above. This works great for an image using a <img> tag, but <svg>
tags cannot use this method.
<a href="#!">
<svg role="img">
<title>Connect on our Twitter page.</title>
<path>...</path>
</svg>
</a>
Issue 6: Color contrast {: #color-contrast }
Background and foreground colors don't have a sufficient contrast ratio. Low-contrast text is difficult or impossible for many users to read. Learn more about color contrast rules.
Two examples were reported.
#01aa9d and the background hex value is #ffffff. The color contrast ratio is 2.9:1.
Mermaid syndrome has a text hex value of #7c7c7c, while the background's hex color is #ffffff. The color contrast ratio is 4.2:1.
<a href="https://web-dev.imgix.net/image/VbsHyyQopiec0718rMq2kTE1hke2/86Iongt2UcohbzEar4Pm.png">View full size screenshot</a>.
</figcaption>
There are many color contrast issues detected on the web page. As you learned in the color and contrast module, regular-sized text (less than 18pt / 24px) has a color contrast requirement of 4.5:1, while large-sized text (at least 18pt / 24px or 14pt / 18.5px bold) and essential icons must meet the 3:1 requirement.
For the page title, the teal-colored text needs to meet the 3:1 color contrast requirement since it is large-sized text at 24px. However, the teal buttons are considered regular-sized text at 16px bold, so they must meet the 4.5:1 color contrast requirement.
In this case, we could find a teal color that was dark enough to meet 4.5:1, or we could increase the size of the button text to 18.5px bold and change the teal color value slightly. Either method will stay in line with the design aesthetics.
All the gray text on the white background also fails for color contrast, except for the two largest headings on the page. This text must be darkened to meet the 4.5:1 color contrast requirements.
#008576 and the background remains #ffffff. The updated color contrast ratio is 4.5:1.
<a href="https://web-dev.imgix.net/image/VbsHyyQopiec0718rMq2kTE1hke2/2JyEvvfRBNFr7YdPipLf.png">View full size screenshot</a>.
<figcaption>
<code><b>Mermaid syndrome</b></code> now has a color value of <code>#767676</code> and the background remains <code>#ffffff</code>. The color contrast ratio is 4.5:1.
<a href="https://web-dev.imgix.net/image/VbsHyyQopiec0718rMq2kTE1hke2/3DgVWG6oIRRVTGYmZP3c.png">View full size screenshot</a>.
</figcaption>
Issue #7 - list structure {: #list-structure }
List items (<li>) are not contained within <ul> or <ol> parent elements.
Screen readers require list items (<li>) to be contained within a parent
<ul> or <ol> to be announced properly.
<div class="ul">
<li><a href="#">About</a></li>
<li><a href="#">Community</a></li>
<li><a href="#">Donate</a></li>
<li><a href="#">Q&A</a></li>
<li><a href="#">Subscribe</a></li>
</div>
We used a CSS class in this demo to simulate the unordered list instead of
using a <ul> tag. When we wrote this code improperly, we removed the inherent
semantic HTML features built into this tag. By replacing the class with a real
<ul> tag and modifying the related CSS, we resolve this accessibility issue.
<ul>
<li><a href="#">About</a></li>
<li><a href="#">Community</a></li>
<li><a href="#">Donate</a></li>
<li><a href="#">Q&A</a></li>
<li><a href="#">Subscribe</a></li>
</ul>
Issue #8 - tabindex {: #tabindex }
Some elements have a [tabindex] value greater than 0. A value greater than 0 implies an explicit navigation ordering. Although technically valid, this often creates frustrating experiences for users who rely on assistive technologies. Learn more about tabindex rules.
<button type="submit" tabindex="1">Subscribe</button>
Unless there is a specific reason to disrupt the natural tabbing order on a web
page, there is no need to have a positive integer on a tabindex attribute. To
keep the natural tabbing order, we can either change the tabindex to 0 or
remove the attribute altogether.
<button type="submit">Subscribe</button>
Step 6
Now that you've fixed all the automated accessibility issues, open up a new debug mode page. Run the Lighthouse accessibility audit again. Your score should be much better than on the first run.
We've applied all of these automated accessibility updates to a new CodePen.
Next step
Great job. You have accomplished a lot already, but we haven't finished yet! Next, we'll move on to manual checks, as detailed in the manual accessibility testing module.
{% Assessment 'automated' %}
Manual accessibility testing
{% Aside %} This module on manual accessibility testing is a continuation of the previous module, automated accessibility testing. If you have not yet completed the exercises in that module, we encourage you to do so. This module starts where the previous one left off, focusing on manual accessibility testing tools and techniques. {% endAside %}
Manual testing basics
Manual accessibility testing uses keyboard, visual, and cognitive tests, tools, and techniques to find issues that automated tooling cannot. As automated tooling does not cover all of the success criteria identified in WCAG, it's vital that you do not run automated accessibility tests and then stop testing!
As technology advances, more tests could be covered by automated tooling alone, but today, both manual and assistive technology checks need to be added to your testing protocols to cover all of the applicable WCAG checkpoints.
{% Aside %} Each automated testing tool can have distinct accessibility rulesets and use them in different ways. The actual percentage of WCAG checkpoints covered will vary by tool and the content being tested. {% endAside %}
Pros of manual accessibility tests:
- Reasonably straightforward and quick to run
- Catch a higher percentage of issues than automated tests alone
- Little tooling and expertise needed for success
Cons of manual accessibility tests:
- More complex and time-consuming than automated tests
- May be difficult to repeat at scale
- Require more accessibility expertise to run tests and interpret the results
Let's compare what accessibility elements and details can currently be detected by an automated tool, versus those that won't be detected.
Types of manual tests
There are many manual tools and techniques to consider when looking at your web page or app for digital accessibility. The three biggest focus areas in manual testing are keyboard functionality, visually-focused reviews, and general content checks.
We will cover each of these topics at a high level in this module, but the following tests are not meant to be an exhaustive list of all the manual tests you can or should run. We encourage you to start with a manual accessibility checklist from a reputable source and develop your own focused manual testing checklist for your specific digital product and team needs.
{% Aside %} Some organizations consider assistive technology (AT) checks to be part of the manual testing process as there are a lot of overlaps. In this course, we break AT testing into a separate module, as it's more advanced than other manual tests and deserves a deeper and separate focus. {% endAside %}
Keyboard checks
It's estimated that about 25% of all digital accessibility issues are related to a lack of keyboard support. As we learned in the keyboard focus module, this affects all types of users, including sighted keyboard-only users, low-vision/blind screen reader users, and people using voice recognition software that uses technology that relies on content being keyboard accessible as well.
Keyboard tests answer questions such as:
- Does the web page or feature require a mouse to function?
- Is the tabbing order logical and intuitive?
- Is the keyboard focus indicator always visible?
- Can you get stuck in an element that shouldn't trap focus?
- Can you navigate behind or around an element that should be trapping focus?
- When closing an element that received focus, did the focus indicator return to a logical place?
While the impact of keyboard functionality is huge, the testing procedure is quite simple. All you need to do is set aside your mouse or install a small JavaScript package and test your website using only your keyboard. The following commands are essential for keyboard testing.
Visual checks
Visual checks focus on visual elements of the page and utilize tools such as screen magnification or browser zoom to review the website or app for accessibility.
Visual checks can tell you:
- Are there color contrast issues that an automated tool could not pick up, such as text on top of a gradient or image?
- Are there any elements that look like headings, lists, and other structural elements but are not coded as such?
- Are navigation links and form inputs consistent throughout the website or app?
- Is there any flashing, strobing, or animation that exceeds the recommendations?
- Does the content have proper spacing? For letters, words, lines, and paragraphs?
- Can you see all the content using a screen magnifier or browser zoom?
{% Aside %} Sometimes, you can't observe the accessibility of a visual element without additional help. You'll read more about that in our next module on assistive technology testing. {% endAside %}
Content checks
Unlike visual tests that focus on layouts, movement, and colors, content checks focus on the words on the page. Not only should you be looking at the copy itself, but you should review the context to be sure it makes sense to others.
Content checks answer questions such as:
- Are page titles, headings, and form labels clear and descriptive?
- Are image alternatives concise, accurate, and useful?
- Is color alone used as the only way of conveying meaning or information?
- Are links descriptive or do you use generic text such as “read more” or “click here?”
- Are there any changes to the language within a page?
- Is plain language being used and are all acronyms spelled out when first referenced?
Some content checks can be automated, in part. For example, you could write a JavaScript linter that checks for "Click here" and suggests you make a change. However, these custom solutions often still need a human to change the copy to something contextual.
Demo: Manual test
So far, we have run automated tests on our demo web page and found and remediated eight different issue types. We are now ready to run manual checks to see if we can discover even more accessibility issues.
Step 1
Our updated CodePen demo has all of the automated accessibility updates applied.
View it in debug mode to proceed with the
next tests. This is important, as it removes the <iframe> which surrounds the
demo web page, which may interfere with some testing tools. Learn more about
CodePen's debug mode.
Step 2
Start your manual testing process by setting your mouse or trackpad aside and navigate up and down the DOM using only your keyboard.
Issue 1: Visible focus indicator {: #visible-focus-indicator}
You should see the first keyboard issue right away—or rather, you shouldn't see it—as the visible focus indicator has been removed. When you scan the CSS in the demo, you should find the dreaded “outline: none” added to the codebase.
:focus {
outline: none;
}
As you learned in the Keyboard focus module, you need to remove this line of code to allow web browsers to add a visible focus for users. You can go one step further and create a focus indicator styled to meet the aesthetics of your digital product.
:focus {
outline: 3px dotted #008576;
}
Issue 2: Focus order {: #focus-order}
Once you have modified the focus indicator and it's visible, be sure to tab through the page. As you do so, you should notice that the form input field used to subscribe to the newsletter does not receive focus. It has been removed from the natural focus order by a negative tabindex.
<input type="email" placeholder="Enter your e-mail address" aria-hidden="true" tabindex="-1" required>
Since we would like people to use this field to sign-up for our newsletter, all we need to do is remove the negative tabindex or set it to zero to allow the input to become keyboard focusable again.
<input type="email" placeholder="Enter your e-mail address" aria-hidden="true" required>
Step 3
Once keyboard focus has been checked, we move on to visual and content checks.
Issue 3: Link color contrast {: #link-color-contrast}
As you went through the keyboard tests by tabbing up and down the demo page, you probably noticed the keyboard focused on three visually hidden links in the paragraphs about the different medical conditions.
For our page to be accessible, links must stand out from the surrounding text and include a non-color style change on mouse hover and keyboard focus.
A quick solution is to add an underline to the links inside the paragraphs to make them stand out. This would solve the accessibility issue, but it might not suit the overall design aesthetics you hope to achieve.
If you choose not to add an underline, you will need to modify the colors in such a way as to meet the requirements for both the background and copy.
When looking at the demo using a link contrast checker tool, you will see that the link color meets the 4.5:1 color contrast requirement between regular-sized text and the background. However, non-underlined links must also meet a 3:1 color contrast requirement against the surrounding text.
One option is to change the link color to match the other elements on the page. But if you change the link color to green, the body copy must also be modified to meet the overall color contrast requirements between all three elements: links, background, and surrounding text.
Issue 4: Icon color contrast {: #icon-color-contrast}
Another missed color contrast issue is the social media icons. In the color and contrast module, you learned that essential icons need to meet a 3:1 color contrast against the background. However, in the demo, the social media icons have a contrast ratio of 1.3:1.
To meet the 3:1 color contrast requirements, the social media icons are changed to a darker gray.
{% Aside %}
You may notice that the border around the text input doesn't meet the 3:1 color contrast requirement against the background. However, this input has placeholder text which meets the required color contrast requirements for its size, according to the non-text contrast overview page.
{% endAside %}
Issue 5: Content layout {: #content-layout}
If you look at the layout of the paragraph content, the text is fully justified. As you learned in the Typography module, this creates "rivers of space," which may make the text difficult for some users to read.
p.bullet {
text-align: justify;
}
To reset the text alignment in the demo, you can update the code to
text-align: left; or remove that line entirely from the CSS, as left is the
default alignment for browsers. Be sure to test the code, in case other
inherited styles remove the default text alignment.
p.bullet {
text-align: left;
}
Step 4
Once you've identified and fixed all the manual accessibility issues outlined in the previous steps, your page should look similar to our screenshot.
It's possible that you'll find more accessibility issues in your manual checks than we covered in this module. We'll discover many of these issues in the next module.
Next step
Way to go! You have completed the automated and manual testing modules. You can view our updated CodePen, which has all the automated and manual accessibility fixes applied.
Now, head over to the last testing module focused on assistive technology testing.
{% Assessment 'manual' %}
Typography
Creating and designing accessible content is more than just choosing an easy-to-read font. Even with accessible font families, people with low vision, cognitive, language, and learning disabilities may struggle to process the text due to other elements such as font variations, size, spacing, and kerning—to name a few. This module will look into basic design considerations to make your content more inclusive and reach even more people.
{% Aside %} To learn specific implementation of typography, check out the Learn CSS Typography module. {% endAside %}
Typeface
A major factor that can strongly impact copy accessibility is the typeface. Your choice of typeface and styling can make or break any page design.
People with reading, learning, and attention disorders like dyslexia and attention-deficit hyperactivity disorder (ADHD), as well as people with low vision, can all benefit when you use accessible typefaces.
Choose common typefaces The quickest way to create an accessible design is to choose a common typeface (for example, Arial, Times New Roman, Calibri, Verdana, and many others).
Many typeface studies testing people with disabilities show that common typefaces lead to faster reading speeds and a deeper comprehension level when compared to uncommon typefaces. While these common typefaces are not inherently more accessible than other typefaces, some people with disabilities have an easier time reading them because they have had a lot of experience working with (or around) these typefaces.
In addition to choosing a common typeface, be sure to avoid ornate or handwritten typefaces, as well as ones with only one character case available (for example, only uppercase characters). These specialty typefaces with cursive designs, quirky shapes, or artistic features like thin lines may look nice, but they are much harder for some people with disabilities to read than common typefaces.
{% Codepen { user: 'web-dev-codepen-external', id: 'ExLvWOV', height: 350, theme: 'auto', tab: 'css,result' } %}
Letter characteristics and kerning
The research on whether serif or sans serif typefaces are easier to read is inconclusive, but certain numbers, letters, or combinations can confuse people with language-based learning and cognitive disabilities. For people with these types of disabilities, every letter and number must be clearly defined and have unique characteristics, so letters are not confused with a numbers.
Common readability offenders are the uppercase "I" (India), lowercase "l" (lettuce), and the number "1". Likewise, letter pairs like b/d, p/q, f/t, i/j, m/w, and n/u can sometimes flip either left-right or up-down for some readers.
The copy's readability also decreases when the letter spacing or kerning is too tight. Pay special attention to kerning, especially between the problematic letter pair r/n. Otherwise, words like "yarn" could change to "yam" or "stern" to "stem," entirely changing the meaning of the copy.
Open source typeface collections like Google Fonts can aid you in selecting the most inclusive typeface for your next design. If you use Adobe products, you can embed accessible font families from foundry partners directly into your designs—this includes select Google Fonts.
{% Codepen { user: 'web-dev-codepen-external', id: 'BaxdWvV', height: 350, theme: 'auto', tab: 'css,result' } %}
When you are looking for your next typeface, pay particular attention to the following:
- Use common fonts whenever possible.
- Avoid using elaborate or handwritten fonts and those with only one character case.
- Pick a typeface with unique characteristics—paying special attention to the uppercase I, lowercase l, and 1.
- Review certain letter combinations to be sure they are not an exact mirror image of one another.
- Check the kerning, especially between the r/n letter pair.
Font size and typographic styling
People often assume that picking out an accessible font family is all there is to creating inclusive content, but it is also important to consider the font size and how the text is styled on a page.
For example, people with low vision or color blindness may be unable to read some of the copy if it is too small, using an AT—like browser zoom—to read the copy. While other users, such as those with dyslexia or reading disorders, may have trouble reading italic text. Screen readers often ignore styling methods, such as bold and italics, so the intent of these styles is not conveyed to blind or low-vision users.
{% Compare 'better' %}
h2 {font-size: 1rem;}
{% endCompare %}
Since you cannot predict what every user's needs are, when adding fonts to your digital products, be sure to consider the following guidelines:
- Base font sizes should be defined with a relative value (%, rem, or em) to allow easy resizing.
- Limit the number of typeface variations like color, bold, ALL CAPS, and italics to increase readability. Instead, use methods to emphasize words in your copy, such as asterisks, dashes, or highlighting an individual word.
- Use markup instead of text on images whenever possible. Screen readers cannot read embedded text on images (without extra code added), and embedded text can also become pixelated when magnified by low-vision users.
Structure and layout
While typeface, font size, and typographic styling are important to accessible typography, the structure and layout of copy on a page can be equally important to a user's understanding.
Complex layouts can be a real barrier for people with low vision, reading disabilities, and the 6.1 million people in the US with ADHD. These types of disabilities make it more difficult for people to keep their place and follow the flow of the copy due to the lack of clear linear pathways, missing headings, and ungrouped elements.
An important aspect of accessible layout designs is making critical elements distinct from one another and grouping similar elements together. If the elements are too close, it can be difficult to tell where one element begins and ends, especially if they have similar styling.
Think about your copy as a collection of individual bullet points on an outline. This will help you plan out the overall page structure and enable you to use headings, subheadings, and lists whenever appropriate.
{% Codepen { user: 'web-dev-codepen-external', id: 'poVreqB', height: 450, theme: 'auto', tab: 'result' } %}
Spacing
Paragraph, sentence, and word spacing is also important as it helps readers retain their focus on the copy and adds to the page's overall visual understanding. Long lines of copy can be a barrier for readers with disabilities, as they have trouble keeping their place and following the flow of the copy. A narrow block of copy makes it easier for readers to continue to the next line.
{% Codepen { user: 'web-dev-codepen-external', id: 'ExLvWrP', height: 350, theme: 'auto', tab: 'result' } %}
Content alignment
Another frustration for many people with disabilities is reading justified copy. The uneven spacing between words in justified copy can cause "rivers of space" to form down the page, making the copy difficult to read.
Text justification can also cause words to be either bunched together or stretched in unnatural ways, so readers can find it difficult to locate word boundaries.
{% Codepen { user: 'web-dev-codepen-external', id: 'OJZjpdE', height: 350, theme: 'auto', tab: 'result' } %}
Thankfully, there are clear guidelines on spacing and tools such as Good Line-Height and the Golden Ratio Calculator to help make our copy more accessible. Incorporating these guidelines helps people with attention-deficit disorders, reading, and vision-based disabilities focus more on the copy and less on the layout.
Best practices for structure and layout
When considering structure and layout, be sure to:
- Use elements like headings, subheadings, lists, numbers, quote blocks, and other visual groupings to break the page into sections.
- Use clearly defined paragraphs, sentences, and word spacing.
- Build columns of copy that do not exceed 80 characters in width (40 characters for logograms).
- Avoid justified paragraph alignment, which creates "rivers of space" within the copy.
Accessible typography takeaways
Accessible typography can be boiled down to common-sense design choices based on your knowledge of your users' needs. Keeping this module in mind as you design and build out your content will go a long way toward helping you communicate clearly with the greatest number of people.
{% Aside %} Refer to the Typography module for Learn CSS to learn how to implement certain rules and styles. {% endAside %}
{% Assessment 'typography' %}
Video and audio
Have you ever wanted to watch a live event but couldn't find your headphones, so you turned on the captions? Or maybe you didn't quite catch the last few talking points from your favorite podcast, so you decided to read the transcript instead? If so, you probably understand the importance and convenience of having alternative ways to access audio and video content.
While your role at your company or organization may not require you to create audio and video content directly, it's important to know the basics of accessibility requirements for media. This knowledge will help you design and build the appropriate layouts and features to accommodate users with different environmental and sensory needs, such as the millions of people with hearing loss or visual impairment worldwide.
Alternative media types
Alternative media types were developed to support the media needs of people with disabilities. This gives people additional formats to choose from when accessing audio and video content.
The alternative media types you must include with your media files depend on:
- The type of media you are supporting—audio-only, video-only, or video with audio (multimedia) formats
- Whether the media is live or pre-recorded
- The version and level of WCAG compliance you are targeting
- Any additional media-related user needs
To create accessible audio and video content for websites and apps, there are four main types of alternative media types: captions, transcripts, audio descriptions, and sign language interpretation.
Captions
One of the most widely used alternative media types are captions. Captions are written text synchronized with multimedia content for people who can't hear or understand spoken words. They are presented in the same language as the main audio track and include important non-speech information, such as sound effects, background noises, and essential music.
Captions benefit people who are deaf, hard of hearing, or have cognitive disabilities, but are useful to many other people as well.
Captions come in two forms—open or closed.
- Closed captions (CC) are text on top of a video that can be turned on or off by the viewer and, depending on the media player, styled in a way that fits the user's needs.
- Open captions (OC) are text burned into the video and cannot be turned off or styled differently.
One method might be preferable, depending on the situation or how the multimedia will be consumed.
People often confuse captions with subtitles, but they aren't synonymous. Both are text synchronized with multimedia content, often appearing at the bottom of the media. Captions can be thought of as a transcription of dialogue and other essential sounds for people with disabilities. Subtitles are visual text for people who can hear the audio track but may not understand what was said, such as when watching a foreign language film.
{% Aside %} There are some geographical differences in what are considered captions and subtitles, so be sure to check the terminology in your location. {% endAside %}
| Features | Subtitles | Closed captions | Open captions |
|---|---|---|---|
| Visual text matches audio track | No | Yes | Yes |
| Includes essential background sounds | No | Yes | Yes |
| Ability to toggle on/off | Yes | Yes | No |
{% Details %} {% DetailsSummary %} Compare screenshots from this video, with and without captions. {% endDetailsSummary %}
Transcripts
Close cousins to captions, transcripts are detailed, text-based documents that capture all essential words, sounds, and important visual information in your media. Transcripts primarily help people who are hard of hearing or deaf, and descriptive transcripts help people who are deafblind.
Transcripts are also useful for people with cognitive disabilities or for people who want to review the content at their own speed.
While transcripts are typically more detailed than captions, they are very similar in format and purpose. They are so similar that many people first add captions to their media, export them, and then use them as the foundation of their transcripts. Repurposing your captions to build your transcripts saves time versus creating everything from scratch.
Search bots can't access your captions but can crawl your text transcripts. When you include transcripts with your media files, your search engine optimization gets a boost. It's one of those rare exceptions when duplicate content isn't confusing to users or penalized by search engine algorithms.
Every media player handles transcripts in a different way. Some providers may not have that functionality built into their media player, and even when they do, some users may not be able to access the transcript interface. You can ensure you've made your transcript available to all users by:
- Including the transcript text directly in-context, on the page with the embedded video.
- Adding a link to an accessible PDF containing the transcript.
- Linking out to the copy on another page.
- Including a link to the transcript, wherever it lives, within the video description on whatever media player platform you've used (such as YouTube or Vimeo).
For example, visit YouTube to watch Password Problems? | There's no place like Chrome and review an example of a transcript.
Audio descriptions
Another alternative media used to support people with disabilities is audio description. This type of alternative media utilizes a narrator to explain important visual information to people who can't see the visual content. These descriptions include nonverbal information such as facial expressions, unspoken actions, and the background environment in video-only and multimedia content.
Sometimes audio descriptions need to be very detailed due to the large amount of information that needs to be shared with the viewer. If there aren't enough natural pauses in the video for audio descriptions, extended audio descriptions are used. In extended audio descriptions, a video will pause to give a narrator enough time to convey all the information in the media before playing the rest of the video.
Audio descriptions and extended audio descriptions help people who are blind or have low vision, but could help people with some cognitive disorders as well.
[Audio Described] Get started with Lookout from Google | Android.
Sign language interpretation
Another major alternative media type you may encounter is sign language interpretation, where an interpreter narrates the auditory portion of the audio-only or multimedia content using sign language. This is very important for many people who are deaf, as sign language is their first and most fluent language.
Sign language interpretation is often more expressive and detailed than written documents, providing a much richer experience than captions or transcripts alone.
That said, sign language interpretation can be time-consuming and cost-prohibitive to many organizations. And even if you have the time and budget to add sign language interpretation to your media, there are over 300 different sign languages worldwide. Adding one sign language interpretation to your media wouldn't be enough to support a global audience.
What is digital accessibility, and why does it matter?
Imagine a world where you couldn't buy a present for a friend because the online shopping cart was incompatible with your device. Or a world where you had to ask a coworker to help you understand the recent sales chart because it only used soft monotone colors. Maybe you couldn't enjoy that new show everyone's been talking about because the captions are missing or badly automated.
For some people, this world is an everyday reality. But it doesn't have to be this way—this is a reality you can help change when you make digital accessibility a priority. Digital accessibility, commonly abbreviated to a11y, is about designing and building digital products so that, regardless of a person's disability, they can still interact with the product in a meaningful and equivalent way.
Beyond the typical leadership buy-in, time, effort, and budget that are required of any project, building digital products with full inclusivity in mind also requires:
- Expert knowledge of various accessibility standards
- Understanding the fundamentals of accessible designs and code
- Understanding the importance of using multiple testing techniques and tools
Most importantly, true inclusivity can only come when you include people with disabilities and accessibility best practices into the full product lifecycle—from planning, to designing, to coding, and more.
What is the individual impact?
The World Health Organization (WHO) estimates that over 15% of the world's population—or 1.3 billion people—self-identify as having a disability, making this group the largest minority group globally.
More recent reports from the Centers for Disease Control and Prevention (CDC), the US Census, the Academic Network of European Disability experts (ANED), and others estimate the total number of people with disabilities to be even greater. This number continues to grow as the world population ages and faces chronic health conditions.
{% Img src="image/VbsHyyQopiec0718rMq2kTE1hke2/wmi25b63ncEEhKTGYBUS.png", alt="Six people representing various disabilities. Each character will be represented.", width="800", height="200" %}
Inaccessible digital products impact people with disabilities. Some types of disabilities are impacted more in the digital world than others.
{% Aside %} To learn more about identity and language choices, we recommend reading Demystifying Disability: What to Know, What to Say, and How to be an Ally by Emily Ladau. {% endAside %}
{% Details 'open' %}
{% DetailsSummary 'h3' %} Visual impairments {% endDetailsSummary %}
Visual impairment (vision impairment, vision disability) is a decreased ability to see to the degree that causes problems not fixable by usual means, such as glasses or medication. Visual impairment can be due to disease, trauma, or congenital or degenerative conditions.
- Examples: B/blindness, low vision, color blindness
- Prevalence: 253 million people with visual impairment worldwide—36 million are blind, 217 million have moderate to severe visual impairment (MSVI) (Source), and 1 in 12 men and 1 in 200 women are colorblind. (Source)
- Tools include: Screen reader software, screen magnification tools, Braille output devices.
- Pain points: Digital products that do not work with screen reader software, mobile websites/apps without pinch to zoom, complex graphs and charts differentiated by colors alone, color contrasts that make it difficult to read text on the screen
{% Aside 'important' %} Use lowercase when referring to a vision-loss condition or to a blind person who prefers lowercase. Capitalize for those who capitalize Blind when describing themselves. {% endAside %}
"My vision has deteriorated rapidly over the last three years, and my phone's default font size ranges from large to mega-large. There are a decent amount of mobile apps that I can barely use because of their absurd font sizes."
Frank
Read a short article in the New York Times or watch a video on what it means to be legally blind.
{% endDetails %}
{% Details %} {% DetailsSummary 'h3' %} Mobility impairments {% endDetailsSummary %}
Mobility impairment is a category of disability that includes people with various physical disabilities. This type of disability includes upper or lower limb loss or disability, manual dexterity, and disability in coordination with different organs of the body.
- Examples: Arthritis, paralysis, amputees, seizure disorders.
- Prevalence: 1 in 7 people have mobility issues. (Source)
- Tools include: Adaptive switches, eye tracking devices, mouth/head sticks, speech input.
- Pain points: Elements that are only designed to work with the use of a mouse.
"Accessibility isn't just for people with disabilities. I had elbow surgery, and it temporarily changed how I managed my daily digital activities."
Melissa
{% endDetails %}
{% Details %}
{% DetailsSummary 'h3' %} Hearing impairments {% endDetailsSummary %}
A hearing impairment or hearing loss is a full or partial decrease in the ability to detect or understand sounds. Hearing impairments are caused by a wide range of biological and environmental factors.
- Examples: D/deafness, hard of hearing (HoH), hearing impaired (HI)
- Prevalence: Globally, over 1.5 billion people have a low to medium level of hearing loss, while it is estimated that 66 million people have a significant level of hearing loss.
- Tools include: hearing aids, captions, transcripts, sign language.
- Pain points: Audio content without text transcripts, video with no synchronized captions
{% Aside 'important' %} Use lowercase when referring to a hearing-loss condition or to a deaf person who prefers lowercase. Capitalize for those who identify as members of the Deaf community or when they capitalize Deaf when describing themselves. {% endAside %}
"Some deaf people say auto-captions are NOT better than nothing. Some deaf people say automatic captions ARE better than nothing. Unlike people with hearing, deaf people have nothing to fall back on. All they have is the captions. Personally, I'd rather see no captions than watch automatic captions. Sure, I'm disappointed there aren't captions. With no auto-captions, I avoid the painful experience of its notoriously bad captions."
Meryl
{% endDetails %}
{% Details %} {% DetailsSummary 'h3' %} Cognitive impairments {% endDetailsSummary %}
A cognitive disability covers a variety of medical conditions affecting cognitive ability. People with cognitive disabilities include various intellectual or cognitive deficits, deficits too mild to properly qualify as intellectual disability, specific conditions, and problems acquired later in life through acquired brain injuries or neurodegenerative diseases like dementia.
- Examples: Down's syndrome, A/autism, ADHD, dyslexia, aphasia.
- Frequency: Varies by condition.
- Tools include: Screen readers, text highlighting, text prediction, abstractive summarization tools.
- Pain points: Busy interfaces that make it overly complicated to focus on the task at hand, big walls of words with little whitespace, justified text, and small or hard-to-read fonts.
{% Aside 'important' %} Use lowercase when referring to autism as a disorder or to an autistic person who prefers lowercase. Capitalize for those who capitalize Autism or Autistic when describing themselves. {% endAside %}
"Right now, I am recovering from an ocular migraine, and I would say dark mode is not helping enough. I still need contrast, but less harshly bright."
Ruth
Read a short article in the New York Times or watch a video on face blindness (prosopagnosia).
{% endDetails %}
{% Details %} {% DetailsSummary 'h3' %} Seizure and vestibular disorders {% endDetailsSummary %}
A seizure is an excessive surge of electrical activity in the brain that can cause various symptoms, depending on which parts of the brain are involved. Seizures may result from genetics or a brain injury, but their cause is often unknown.
The vestibular system includes the parts of the inner ear and brain that process the sensory information that controls balance and eye movements. If disease or injury damages these processing areas, vestibular disorders can result. Vestibular disorders can also result from or be worsened by genetic or environmental conditions or occur for unknown reasons.
- Examples: Epilepsy, vertigo, dizziness, labyrinthitis, balance, and eye movement disorders.
- Frequency: 50 million people worldwide have epilepsy, and 1.8 million adults worldwide have bilateral vestibular hypofunction (BVH).
- Tools include: Operating system settings to reduce motion. In Windows, this setting is framed positively as Show animation, and is turned off. On Android, the setting Remove animations is turned on.
- Pain points: Videos that autoplay, extreme flashing or strobing of visual content, parallax effects, or scroll-triggered animations.
"I really dislike the superfluous animation that plagues iOS transitions between apps, so I turn it off. Downside: I'm denied most of the thoughtfully executed motion design on the web because there's no “some motion is fine” middle ground."
Oliver
{% endDetails %}
{% Details %} {% DetailsSummary 'h3' %} Speech impairments {% endDetailsSummary %}
A speech disorder is a condition in which a person has problems creating or forming the speech sounds needed to communicate with others.
- Examples: Muscular or cognitive issues that impede speech, such as apraxia, dysarthria, or stuttering.
- Frequency: 18.5 million individuals have a speech, voice, or language disorder.
- Tools include: Augmentative and alternative communication (AAC) and speech-generating devices.
- Pain points: Voice-activated technology such as smart home devices and apps.
"My son has a lisp due to dyspraxia. He will say "seep" rather than "sheep" or "fower" rather than "flower". It is sweet, but he gets so frustrated by voice-activated software.
Our new car uses voice activation to interact with a phone. Often if we are together, my husband will send us a WhatsApp message. The car will read it out loud, but when it asks us if we want to reply, my son's reply is not understood. He gets so upset... he now whispers the message to me so I can say it for the reply."
Helen
Read a short article in the New York Times or watch a video on stuttering and technology.
{% endDetails %}
Additional beneficiaries of accessibility
While the number of people with disabilities worldwide is large, it's important to remember that these numbers aren't inclusive of everyone that benefits from accessible digital spaces. This includes:
- Temporarily disabled. It may mean someone has a broken wrist or is cognitively impaired due to medication.
- Situationally disabled. For example, someone experiencing glare on a device screen or being unable to play the audio on a video in a public setting.
- Mildly disabled. A person needing eyeglasses to see a screen or captions to understand audio.
- Non-native speakers. If a person is not fluent in the language on the screen, they may need more time to read content on a slide on a carousel/slideshow.
- Older people with age-related diminishing senses. It could be a person needing reading glasses or bifocals to read small print or requiring a larger target size for buttons on their touch device due to an age-related hand tremor.
- Search engine optimization (SEO) bots. SEO bots do not have senses like sight and hearing and navigate by keyboard only. Your websites will be crawled more effectively when your site is accessible.
Business impact
People with disabilities make up almost a fourth of the world's population, but did you know that they also have a lot of spending power?
According to the American Institutes for Research (AIR), the total after-tax disposable income for working-age Americans with disabilities is about $490 billion annually. This number is similar to other significant market segments in the US, such as the Black ($501 billion) and Latinx ($582 billion) communities. Companies that do not plan for, design, and build accessible products can lose out on this potential revenue.
While these numbers are impressive, people with disabilities are also part of a larger network of family members, friends, communities, and institutions. This larger network often looks for and supports businesses that create accessible digital products. When you factor in the friends and family of the over 1.3 billion people worldwide who identify as disabled, the disability market touches 53% of all consumers. It's the world's largest emerging market.
In addition to money and market shares, businesses focused on disability inclusion as part of an overall diversity strategy are higher performing and more innovative. There are many examples of everyday products that evolved from technology developed by, or for, people with disabilities, including:
- Telephones
- Typewriters / keyboards
- Kitchen utensils
- Easy-open pull-out drawers
- Automatic door openers
- Voice controls
- Eye gaze technology
When we look at accessibility as a design or coding challenge, not a begrudging requirement, innovation is the byproduct. For people without disabilities, such improvements can increase the overall user experience. For people with disabilities, these improvements are essential for equal access.
Legal impact
Beyond the individual and business impact, you should also be aware of the looming legal impact of not building accessible digital products. Public sector entities in the United States, such as government-funded programs/schools, airlines, and nonprofits, must follow certain digital accessibility rules, while many private sector companies do not. In countries such as Canada, the United Kingdom, Japan, Australia, and the European Union, stricter digital accessibility laws exist for both public and private companies.
For many disabled people in the US, filing a lawsuit is their only option to bring awareness and change to digital products. It is estimated that in the US, over ten lawsuits are filed daily focused on digital accessibility. Many businesses have received multiple digital accessibility-based lawsuits. And every year, the number of total lawsuits has increased.
E-commerce websites and apps are typically the biggest targets, comprising over 74% of the lawsuits filed in 2021. If your company has both a physical location and an online presence, you are more likely to have been part of a lawsuit. In fact, of the top 500 e-commerce sites, 412 have been served with a lawsuit within the past four years. Often, the first lawsuit is for the company's website and the second for their mobile app.
While avoiding lawsuits shouldn't be the only reason you focus on making sure your digital products are accessible, it is an important piece of the conversation.
{% Assessment 'why' %}
Learn CSS
This course breaks down the fundamentals of CSS into digestible, easy to understand pieces. Over the next few modules, you'll learn how the core aspects of CSS work and how to use them effectively in your projects. Use the menu pane by the "Learn CSS" logo to navigate the modules.
You'll learn CSS fundamentals like the box model, cascade and specificity, flexbox, grid and z-index. And, along with these fundamentals, you'll learn about functions, color types, gradients, logical properties and inheritance to make you a well-rounded front-end developer, ready to take on any user interface.
Each module is full of interactive demos and self-assessments for you to test your knowledge. In addition to learning through reading and demos, there is an accompanying podcast episode for each topic as another way to learn and continue expanding your knowledge.
This course is created for beginner and advanced CSS developers alike. You can go through the series from start to finish to get a general understanding of CSS from top to bottom, or you can use it as a reference for specific styling subjects. For those new to web development overall, check out Learn HTML to learn all about how to write markup and link your stylesheets.
Here's what you'll learn:
{% include 'partials/course-index.njk' %}
So, are you ready to learn CSS? Let's get started.
{% Aside %} This course is written and edited by CSS community experts: Andy Bell, Rachel Andrew, Una Kravets, Adam Argyle, Emma Twersky, Camden Bickel, and Kevin Lozandier. Rob Dodson and Jiwoong Lee shepherded the UX and production build, along with help from Ewa Gasperowicz and Kayce Basques. {% endAside %}
Animations
Sometimes you see little helpers on interfaces that when clicked, provide some helpful information about that particular section. These often have a pulsing animation to subtly let you know that the information is there and should be interacted with. How do you do this with CSS though?
In CSS, you can make this type of animation using CSS animations, which allow you to set an animation sequence, using keyframes. Animations can be simple, one state animations, or even complex, time-based sequences.
What is a keyframe?
In animation software, CSS, and most other tools that enable you to animate something, keyframes are the mechanism that you use to assign animation states to timestamps, along a timeline.
Let's use the "pulser" as a context for this. The entire animation runs for 1 second and runs over 2 states.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/pWCGpgzJqJNTBluK5u8S.svg", alt="The states of the pulser animation over the 1 second timeframe", width="800", height="340" %}
There's a specific point where each of these animation states start and end. You map these out on the timeline with keyframes.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/wmKVq5GFj7cf6jfuLiT8.svg", alt="The same diagram as before, but this time, with keyframes", width="800", height="440" %}
@keyframes
{% BrowserCompat 'css.at-rules.keyframes' %}
Now you know what a keyframe is,
that knowledge should help you understand how the CSS
@keyframes rule works.
Here is a basic rule with two states.
@keyframes my-animation {
from {
transform: translateY(20px);
}
to {
transform: translateY(0px);
}
}
The first part to note is the
custom ident
(custom identifier)—or in more human terms, the name of the keyframes rule.
This rule's identifier is my-animation.
The custom identifier works like a function name. Which, as you learned in the functions module,
lets you reference the keyframes rule elsewhere in your CSS code.
{% Aside %}
A <custom-ident> is used in various places in CSS,
and allows you to provide your own name for things.
These identifiers are case-sensitive,
and in some cases there are words that you can't use.
For example, when naming lines in CSS Grid, you can't use the word span.
{% endAside %}
Inside the keyframes rule, from and to are keywords that represent 0% and 100%,
which are the start of the animation and end.
You could re-create the same rule like this:
@keyframes my-animation {
0% {
transform: translateY(20px);
}
100% {
transform: translateY(0px);
}
}
You can add as many positions as you like along the timeframe. Using the context of the "pulser" example, there are 2 states, which translate to 2 keyframes. This means you have 2 positions inside your keyframes rule to represent the changes for each of these keyframes.
@keyframes pulse {
0% {
opacity: 0;
}
50% {
transform: scale(1.4);
opacity: 0.4;
}
}
The animation properties
{% BrowserCompat 'css.properties.animation' %}
To use your @keyframes in a CSS rule, define various animation properties or, use the
animation shorthand property.
animation-duration
{% BrowserCompat 'css.properties.animation-duration' %}
.my-element {
animation-duration: 10s;
}
The animation-duration
property defines how long the @keyframes timeline should be. It should be a time value.
It defaults to 0 seconds, which means the animation still runs,
but it'll be too quick for you to see.
You can't add negative time values.
animation-timing-function
{% BrowserCompat 'css.properties.animation-timing-function' %}
To help recreate natural motion in animation,
you can use timing functions that calculate the speed of an animation at each point.
Calculated values are often curved,
making the animation run at variable speeds over the course of animation-duration,
and if a value is calculated beyond that of the value defined in @keyframes, make the element appear to bounce.
There are several keywords available as presets in CSS, which are used as the value for
animation-timing-function:
linear, ease, ease-in, ease-out, ease-in-out.
.my-element {
animation-timing-function: ease-in-out;
}
Values appear to curve with easing functions because easing is calculated using a bézier curve,
which is used to model velocity.
Each of the timing function keywords,
such as ease, reference a pre-defined bézier curve.
In CSS, you can define a bézier curve directly,
using the cubic-bezier() function,
which accepts four number values: x1, y1, x2, y2.
.my-element {
animation-timing-function: cubic-bezier(.42, 0, .58, 1);
}
These values plot each part of the curve along the X and Y axis.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/AZr17k5ejTYsikAPiwTm.svg", alt="A bézier on a progression vs time chart", width="800", height="499" %}
Understanding bézier curves is complicated and visual tools, such as this generator by Lea Verou are very helpful.
The steps easing function
Sometimes you might want more granular control of your animation,
and instead of moving along a curve, you want to move in intervals instead.
The steps() easing function lets you break the timeline into defined, equal intervals.
.my-element {
animation-timing-function: steps(10, end);
}
The first argument is how many steps. If steps are defined as 10 and there are 10 keyframes, each keyframe will play in sequence for the exact amount of time, with no transition between states. If there are not enough keyframes for the steps, steps between keyframes are added depending on the second argument.
The second argument is the direction.
If it's set to end, which is the default,
the steps finish at the end of your timeline.
If it is set to start, the first step of your animation completes as soon as it starts,
which means it ends one step earlier than end.
animation-iteration-count
{% BrowserCompat 'css.properties.animation-iteration-count' %}
.my-element {
animation-iteration-count: 10;
}
The animation-iteration-count
property defines how many times the @keyframes timeline should run.
By default, this is 1,
which means that when the animation reaches the end of your timeline,
it will stop at the end.
The number can't be a negative number.
You can use the infinite keyword which will loop your animation,
which is how the "pulser" demo from the start of this lesson works.
animation-direction
{% BrowserCompat 'css.properties.animation-direction' %}
.my-element {
animation-direction: reverse;
}
You can set which direction the timeline runs over your keyframes with animation-direction:
normal: the default value, which is forwards.reverse: runs backwards over your timeline.alternate: for each animation iteration, the timeline will run forwards or backwards in sequence.alternate-reverse: the reverse ofalternate.
animation-delay
{% BrowserCompat 'css.properties.animation-delay' %}
.my-element {
animation-delay: 5s;
}
The animation-delay
property defines how long to wait before starting the animation.
Like the animation-duration property, this accepts a time value.
Unlike the animation-duration property, you can define this as a negative value.
If you set a negative value, the timeline in your @keyframes will start at that point.
For example, if your animation is 10 seconds long and you set animation-delay to -5s, it will start from half-way along your timeline.
animation-play-state
{% BrowserCompat 'css.properties.animation-play-state' %}
.my-element:hover {
animation-play-state: paused;
}
The animation-play-state
property allows you to play and pause the animation.
The default value is running and if you set it to paused, it will pause the animation.
animation-fill-mode
{% BrowserCompat 'css.properties.animation-fill-mode' %}
The animation-fill-mode
property defines which values in your @keyframes timeline persist before the animation starts or after it ends.
The default value is none which means when the animation is complete, the values in your timeline are discarded.
Other options are:
forwards: The last keyframe will persist, based on the animation direction.backwards: The first keyframe will persist, based on the animation direction.both: follows the rules for bothforwardsandbackwards.
The animation shorthand
Instead of defining all the properties separately,
you can define them in an animation shorthand,
which lets you define the animation properties in the following order:
animation-nameanimation-durationanimation-timing-functionanimation-delayanimation-iteration-countanimation-directionanimation-fill-modeanimation-play-state
.my-element {
animation: my-animation 10s ease-in-out 1s infinite forwards forwards running;
}
Considerations when working with animation
Users can define in their operating system that they prefer to reduce motion experienced when they interact with applications and websites. This preference can be detected using the prefers-reduced-motion media query.
@media (prefers-reduced-motion) {
.my-autoplaying-animation {
animation-play-state: paused;
}
}
This isn't necessarily a preference of no animation, but rather, a preference to reduce animations— especially unexpected ones. You can learn more about this preference and overall performance with this animation guide.
{% Assessment 'animations' %}
Backgrounds
Behind every CSS box is a specialized layer called the background layer. CSS provides a variety of ways to make meaningful changes to it–including allowing multiple backgrounds.
Background layers are furthest from the user, rendered behind the contents of a box starting from its padding-box region. This enables the background layer to not overlap with borders at all.
Background color
{% BrowserCompat 'css.properties.background-color' %}
One of the simplest effects you can apply to a background layer is setting the color. The initial value of background-color is transparent, which allows the contents of a parent to be visible. A valid color set on a background layer sits behind other things painted on that element.
Background images
{% BrowserCompat 'css.properties.background-image' %}
On top of the background-color layer, you can add a background image, using the background-image property. A background-image accepts the following:
- An image URL or data URI using the
urlCSS function. - An image dynamically created by a gradient CSS function.
Setting a background-image with the url CSS function
CSS gradient backgrounds
Several gradient CSS functions exist to allow you to generate a background-image, when passed two or more colors.
Regardless of which gradient function is used, the resulting image is intrinsically sized to match the amount of space available.
Demo showing example of applying a background-image using gradient functions:
Repeating background images
{% BrowserCompat 'css.properties.background-repeat' %}
By default, background images repeat horizontally and vertically to fill the entire space of the background layer.
Change this by using the background-repeat property with one of the following values:
repeat: The image repeats within the space available, cropping as necessary.round: The image repeats horizontally and vertically to fit as many instances into the space available, without cropping, compressing, or stretching it.space: The image repeats horizontally and vertically to fit as many instances within the space available without cropping—spacing out instances of the image as needed. Repeating images touch the edges of the space a background layer occupies, with white space evenly distributed between them.
The background-repeat property allows you to set the behavior for the x and y axis independently. The first parameter sets the horizontal repeat behavior, and the second parameter sets the vertical repeat behavior.
If you use a single value, it will be applied to both the horizontal and vertical repeats.
The shorthand also has convenient one-word options to make your intent clearer.
The value repeat-x repeats an image only horizontally; this is equivalent to repeat no-repeat.
The following demo demonstrates these capabilities of the background-repeat property:
Background position
{% BrowserCompat 'css.properties.background-position' %}
You may have noticed when some images on the Web are styled with a background-repeat: no-repeat declaration, such images are displayed top left of their container.
The initial position of background images is top left. The background-position property allows you to change this behavior by offsetting the image position.
As with background-repeat, the background-position property allows you to position images along the x and y axis independently with two values by default.
When CSS lengths and percentages are used, the first parameter corresponds to the horizontal axis while the second parameter corresponds to the vertical axis.
When keywords are only used the order of the keywords does not matter:
{% Compare 'better' %}
background-position: left 50%;
{% endCompare %}
{% Compare 'better' %}
background-position: top left;
{% endCompare %}
{% Compare 'better' %}
background-position: left top;
{% CompareCaption %}
Order does not matter for keywords associated with different axes of position.
{% endCompareCaption %}
{% endCompare %}
{% Compare 'worse' %}
background-position: 50% left;
{% CompareCaption %}
When CSS values are used alongside keywords, the order matters. The first value represents the horizontal axis and the second the vertical axis. {% endCompareCaption %}
{% endCompare%}
{% Compare 'worse' %}
background-position: left right;
{% CompareCaption %}
You cannot use keywords associated with the same axis simultaneously.
{% endCompareCaption %}
{% endCompare %}
The background-position property also has a convenient one value shorthand; the omitted value resolves to 50%. Here's an example that demonstrates this using the keywords the background-position property accepts:
In addition to its default two parameter form and one parameter form; the background-position property also accepts up to four parameters;
When three or four parameters are used, a CSS length or percentage must be preceded by the top, left, right, or bottom keywords in order for the browser to calculate which edge of the CSS box the offset should originate from.
When three parameters are used, a CSS length or value can be the second or third parameter with the other two being keywords; the keyword it succeeds will be used to determine the edge the CSS length or value corresponds to being the offset of. The offset of the other keyword specified is set to 0.
{% Compare 'better' %}
background-position: bottom 88% right;
{% endCompare %}
{% Compare 'better' %}
background-position: right bottom 88%;
{% endCompare %}
{% Compare 'worse' %}
background-position: 88% bottom right;
{% CompareCaption %}
CSS length value must be preceded by the top, right, bottom, or left keywords when using three or more parameters.
{% endCompareCaption %}
{% endCompare %}
{% Compare 'better' %}
background-position: bottom 88% right 33%;
{% endCompare %}
{% Compare 'better' %}
background-position: right 33% bottom 88%;
{% endCompare %}
{% Compare 'worse' %}
background-position: 88% 33% bottom left;
{% CompareCaption%}
CSS length value must be preceded by the top, right, bottom, or left keywords when using three or more parameters.
{% endCompareCaption %}
{% endCompare %}
If background-position: top left 20%is applied to a CSS background image, the image is placed at the top of the box, the 20% value represents a 20% offset from the left of the box (on the x axis).
If background-position: top 20% left is applied to a CSS background image, the 20% value represents a 20% offset from the top of the CSS box (on the y axis), and the image is placed at the left of the box.
When four parameters are used, the two keywords are paired with two values corresponding to an offset against the origins of each keyword specified. If background-position: bottom 20% right 30% is applied to a background-image, the background-image is positioned 20% from the bottom, and 30% from the right of the CSS box.
The following demo demonstrates this behavior:
Here are more examples of using the background-position property using a mix of CSS and keyword values:
{% Aside %}
To learn more about the nuances related to positioning backgrounds,
check out background-position on MDN.
{% endAside %}
Background Size
{% BrowserCompat 'css.properties.background-size' %}
The background-size property sets the size of background images; By default background images are sized based on their intrinsic (actual) width, height, and aspect ratio.
The background-size property uses CSS length and percentage values or specific keywords. The property accepts up to two parameters corresponding to allowing you to change width and height of a background independently.
The background-size property accepts the following keywords:
auto: When used independently, the background image is sized based on its intrinsic width and height; whenautois used alongside another CSS value for the width (first parameter) or height (second parameter), the dimension set toautois sized as needed to maintain the natural aspect ratio of the image. This is the default behavior of thebackground-sizeproperty.cover: Covers the entire area of the background layer. This may mean the image is stretched or cropped.contain: Sizes the image to fill the space without stretching or cropping. As a result, empty space can remain that will cause the image to repeat, unlessbackground-repeatis set tono-repeat.
The latter 2 are intended to be used in a standalone fashion without another parameter.
The following demo demonstrates these keywords in action:
Demo demonstrating applying these keywords to background-size:
Background attachment
{% BrowserCompat 'css.properties.background-attachment' %}
The background-attachment property enables you to modify the fixed position behavior of background images (images part of a background layer) once the layer is visible on a screen.
It accepts 3 keywords: scroll, fixed, and local.
The default behavior of the background-attachment property is the initial value of scroll. When more space is needed, the images move with that space within the background layer determined by the bounds of the CSS box.
Using the value fixed fixes the position of background images to the viewport.
Once the space of the background layer images originally takes up needs to be scrolled (or rendered) offscreen, images within the background layer stay fixed in the original position the background layer enabled them to be until the entire layer is scrolled off screen by the viewport.
The local keyword enables the position of background images to be fixed relative to the element's contents. Background images now move along the space they occupy as that space renders inside and outside the bounds of the CSS box (usually due to scrolling, 2D, or 3D transformations).
Background origin
{% BrowserCompat 'css.properties.background-origin' %}
The background-origin property enables you to modify the area of backgrounds associated with a particular box. The values the property accepts correspond to the border-box , padding-box, and content-box regions of a box .
Try these options out using the following demo:
Background clip
{% BrowserCompat 'css.properties.background-clip' %}
The background-clip property controls what is visually seen from a background layer regardless of the bounds created by the background-origin property.
Like background-origin the regions that can be specified are border-box, padding-box, and content-box corresponding to where a CSS background layer can be rendered. When these keywords are used, any rendering of the background further than the region specified will be cropped or clipped.
The background-clip property also accepts a text keyword that clips the background to be no further than the text within the content box. For this effect to be evident in the actual text within a CSS box, the text must be partially or completely transparent.
A relatively new property, at the time of this writing, Chrome and most browsers require the -webkit- prefix to use this property.
{% BrowserCompat 'css.properties.background-clip' %}
{% Aside 'gotchas' %}
It should be also noted this property is not compatible with background-clip: text being simultaneously set on a CSS box.
{% endAside %}
Multiple backgrounds
As mentioned at the beginning of the module, the background layer allows multiple sublayers to be defined. For brevity, I'll refer to these sublayers as backgrounds.
Multiple backgrounds are defined top to bottom; The first background is the closest to the user, while the last background is the furthest from the user.
The only background defined or the last layer is designated the final background layer by the browser. Only this layer is allowed to assign a background-color.
{% Aside %} With the unpredictability of the web in mind, a background-image may fail to load. Setting a background-color on the final layer ensures good contrast for text and so on if important background layers fail to load. {% endAside %}
Multiple layers can be individually configured using most background-related CSS properties that are comma separated, as demonstrated in the code snippet and live demo below.
background-image: url("https://assets.codepen.io/7518/pngaaa.com-1272986.png"),
url("https://assets.codepen.io/7518/blob.svg"),
linear-gradient(hsl(191deg, 40%, 86%), hsla(191deg, 96%, 96%, 0.5));
background-size: contain, cover, auto;
background-repeat: no-repeat, no-repeat, no-repeat;
background-position: 50% 50%, 10% 50%, 0% 0%;
background-origin: padding-box, border-box, content-box;
The background shorthand
To make it easier to style the background layer of a box-especially when multiple background layers are desired–there is a shorthand that follows the following specific pattern:
background:
<background-image>
<background-position> / <background-size>?
<background-repeat>
<background-attachment>
<background-origin>
<background-clip>
<background-color>?
{% Aside 'gotchas' %} It's important to note that any background properties omitted in the shorthand are set to their initial values. {% endAside %}
Order is important in the shorthand form of declaring multiple backgrounds. The position and size values must both be provided, separated by a slash (/). Declaring the origin and clip behavior in the correct order allows you take advantage of setting keywords that are valid for both simultaneously
The following declaration clips the background, and originates it from the content box:
background: url("https://assets.codepen.io/7518/blob.svg") 50%
50% / contain no-repeat content-box;
With these shorthand semantics in mind, the previous background-related declarations of the code snippet could be rewritten to be following:
background: url("https://assets.codepen.io/7518/pngaaa.com-1272986.png") 50% 50%/contain no-repeat padding-box, url("https://assets.codepen.io/7518/blob.svg") 10% 50% / cover border-box, linear-gradient(hsl(191deg, 40%, 86%), hsla(191deg, 96%, 96%, 0.5) ) 0% 0% / cover no-repeat content-box ;
{% Assessment 'backgrounds' %}
Blend Modes
Duotone is a popular color treatment for photography which makes an image look like it is only made up of two contrasting colors: one for highlights and the other for lowlights. How do you do this with CSS though?
Using blend modes—and other techniques you have learned about, such as filters and pseudo-elements—you can apply this effect to any image.
What is a blend mode?
Blend modes are commonly used in design tools such as Photoshop to create a compositional effect by mixing colors from two or more layers. By changing how colors mix, you can achieve really interesting visual effects. You can also use blend modes as a utility, such as isolating an image that has a white background, so it appears to have a transparent background.
You can use most of the blend modes available in a design tool with CSS,
using the
mix-blend-mode or the
background-blend-mode properties.
The mix-blend-mode applies blending to a whole element
and the background-blend-mode applies blending to the background of an element.
You use background-blend-mode when you have multiple backgrounds on an element
and want them all to blend into each other.
The mix-blend-mode affects the entire element,
including its pseudo-elements.
One use-case is in the initial example of a duotone image,
which has color layers applied to the element through its pseudo-elements.
Blend modes fall into two categories: separable and non-separable. A separable blend mode considers each color component, such as RGB, individually. A non-separable blend mode considers all color components equally.
Browser compatibility
mix-blend-mode
{% BrowserCompat 'css.properties.mix-blend-mode' %}
background-blend-mode
{% BrowserCompat 'css.properties.background-blend-mode' %}
Separable blend modes
Normal
This is the default blend mode and changes nothing about how an element blends with others.
Multiply
The multiply blend mode is like stacking multiple transparencies on top of each other.
White pixels will appear transparent,
and black pixels will appear black.
Anything in between will multiply its luminosity (light) values.
This means lights get much lighter and darks,
darker—most often producing a darker result.
.my-element {
mix-blend-mode: multiply;
}
Screen
Using screen multiplies the light values—an inverse effect to multiply,
and will most often produce a brighter result.
.my-element {
mix-blend-mode: screen;
}
Overlay
This blend mode—overlay—combines multiply and screen.
Base dark colors become darker and base light colors become lighter.
Mid-range colors, such as a 50% gray, are unaffected.
.my-element {
mix-blend-mode: overlay;
}
Darken
The darken blend mode compares the source image and backdrop image's dark color luminosity
and selects the darkest of the two.
It does this by comparing rgb values instead of luminosity (like multiply and screen would do),
for each color channel.
With darken and lighten, new color values are often created from this comparison process.
.my-element {
mix-blend-mode: darken;
}
Lighten
Using lighten does the exact opposite of darken.
.my-element {
mix-blend-mode: lighten;
}
Color dodge
If you use color-dodge, it lightens the background color to reflect the source color.
Pure black colors see no effect from this mode.
.my-element {
mix-blend-mode: color-dodge;
}
Color burn
The color-burn blend mode is very similar to the multiply blend mode,
but increases contrast, resulting in more saturated mid-tones and reduced highlights.
.my-element {
mix-blend-mode: color-burn;
}
Hard light
Using hard-light creates a vivid contrast.
This blend mode either screens or multiplies luminosity values.
If the pixel value is lighter than 50% gray, the image is lightened,
as if it were screened. If it is darker, it's multiplied.
.my-element {
mix-blend-mode: hard-light;
}
Soft light
The soft-light blend mode is a less-harsh version of overlay.
It works in very much the same way with less contrast.
.my-element {
mix-blend-mode: soft-light;
}
Difference
A good way to picture how difference works is a bit like how a photo negative works.
The difference blend mode takes the difference value of each pixel,
inverting light colors.
If the color values are identical, they become black.
Differences in the values will invert.
.my-element {
mix-blend-mode: difference;
}
Exclusion
Using exclusion is very similar to difference,
but instead of returning black for identical pixels,
it will return 50% gray, resulting in a softer output with less contrast.
.my-element {
mix-blend-mode: exclusion;
}
Non-separable blend modes
You can think of these blend modes like HSL color components. Each takes a specific component value from the active layer and mixes it with other component values.
Hue
The hue blend mode takes the hue of the source color
and applies it to the saturation and luminosity of the backdrop color.
.my-element {
mix-blend-mode: hue;
}
Saturation
This works like hue,
but using saturation as the blend mode applies the saturation of the source color
to the hue and luminosity of the backdrop color.
.my-element {
mix-blend-mode: saturation;
}
Color
The color blend mode will create a color from the hue and saturation of the source color
and the luminosity of the backdrop color.
.my-element {
mix-blend-mode: color;
}
Luminosity
Lastly, luminosity is the inverse of color.
It creates a color with the luminosity of the source color and the hue and saturation of the backdrop color.
.my-element {
mix-blend-mode: luminosity;
}
The isolation property
{% BrowserCompat 'css.properties.isolation' %}
If you set the isolation
property to have a value of isolate,
it will create a new stacking context,
which will prevent it from blending with a backdrop layer.
As you learned in the z-index module, when you create a new stacking context,
that layer becomes the base layer.
This means that parent-level blend modes will no longer apply,
but elements inside of an element with isolation: isolate set can still blend.
Note that this doesn't work with background-blend-mode
because the background property is already isolated.
{% Assessment 'blend-modes' %}
Borders
In the box model module, we considered a frame analogy to describe each section of the box model.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/FBaaJXdnuSkvOx1nB0CB.jpg", alt="Three picture frames next to each other. The middle frame has the sections of the box model over the top of it", width="800", height="562" %}
The border box is the frame of your boxes,
and the border properties give you a huge array of options to create that frame in
nearly any style that you can think of.
Border properties
The individual border properties provide a way to style the various parts of a border.
{% BrowserCompat 'css.properties.border' %}
Style
For a border to appear,
you have to define the
border-style.
There's a few options to choose from:
When using the ridge, inset, outset and groove styles,
the browser will darken the border color for the second shown color to provide contrast and depth.
This behaviour can vary between browsers,
especially for dark colors such as black.
In Chrome, these border styles will appear to be solid and in Firefox,
they will be lightened to then provide a darker second color.
Browser behaviour can vary for other border styles too,
so it's important to test your site in different browsers.
A common example of this difference is how each browser renders the dotted and dashed styles.
To set border style on each side of your box,
you can use
border-top-style,
border-right-style,
border-left-style,
and border-bottom-style.
Shorthand
As with margin and padding,
you can use the
border
shorthand property to define all the parts of your border in one declaration.
.my-element {
border: 1px solid red;
}
The order of values in the border shorthand are border-width,
border-style and then, border-color.
Color
You can set color on all sides of your box or on each individual side with
border-color.
By default, it uses the box's current text color: currentColor.
This means that if you only declare border properties,
like width,
the color will be that computed value unless you explicitly set it.
.my-element {
color: blue;
border: solid; /* Will be a blue border */
}
.my-element {
color: blue;
border: solid yellow;
}
To set a border color on each side of your box,
use
border-top-color,
border-right-color,
border-left-color and
border-bottom-color.
Width
The width of a border is how thick the line is, and is controlled by
border-width.
The default border width is medium.
This won't be visible unless you define a style, though.
You can use other named widths such as thin and thick.
The border-width properties also accept a length unit such as
px, em, rem or %.
To set border width on each side of your box, use
border-top-width,
border-right-width,
border-left-width and
border-bottom-width.
Logical properties
In the Logical Properties module you discovered how to refer to block flow and inline flow, rather than explicit top, right, bottom or left sides.
You have this capability with borders, too:
.my-element {
border: 2px dotted;
border-inline-end: 2px solid red;
}
In this example, .my-element has all sides defined as having a 2px,
dotted border that is the current text color.
The inline-end border is then defined as 2px, solid and red.
This means that in left-to-right languages—like English—
the red border will be on the right side of the box.
In right-to-left languages—like Arabic—
the red border will be on the left side of the box.
Browser support is varied for logical properties in borders, so make sure you check support before using.
Border radius
To give a box rounded corners use the
border-radius property.
.my-element {
border-radius: 1em;
}
This shorthand adds a consistent border to each corner of your box.
As with the other border properties,
you can define the border radius for each side with
border-top-left-radius,
border-top-right-radius,
border-bottom-right-radius and
border-bottom-left-radius.
You can also specify each corner's radius in the shorthand, which follows the order: top left, top right, bottom right then bottom left.
.my-element {
border-radius: 1em 2em 3em 4em;
}
By defining a single value for a corner,
you are using another shorthand because a border radius is split into two parts:
the vertical and horizontal sides.
This means that when you set border-top-left-radius: 1em,
you are setting the top-left-top radius and the top-left-left radius.
You can define both properties, per corner like this:
.my-element {
border-top-left-radius: 1em 2em;
}
This adds a border-top-left-top value of 1em,
and a border-top-left-left value of 2em.
This converts the top left border radius into an elliptical radius,
rather than the default circular radius.
You can define these values in the border-radius shorthand,
using a / to define the elliptical values, after the standard values.
This enables you to get creative and make some complex shapes.
.my-element {
border: 2px solid;
border-radius: 95px 155px 148px 103px / 48px 95px 130px 203px;
}
Border images
You don't just have to use a stroke-based border in CSS.
You can also use any type of image, using
border-image.
This shorthand property allows you to set the source image,
how that image is sliced, the image width,
how far the border is outset from the edge and how it should repeat.
.my-element {
border-image-source: url(https://assets.codepen.io/174183/border-image-frame.jpg);
border-image-slice: 61 58 51 48;
border-image-width: 20px 20px 20px 20px;
border-image-outset: 0px 0px 0px 0px;
border-image-repeat: stretch stretch;
}
The border-image-width
property is like border-width:
it is how you set the width of your border image.
The border-image-outset
property lets you set the distance between your border image and the box that it wraps around.
border-image-source
The border-image-source
(source of the border image) can be a url for any valid image, which includes CSS gradients.
.my-element {
border-image-source: url('path/to/image.png');
}
.my-element {
border-image-source: linear-gradient(to bottom, #000, #fff);
}
border-image-slice
The border-image-slice
property is a useful property that allows you to slice an image into 9 parts, made up of 4 split lines.
It works like the margin shorthand where you define the top, right, bottom and left offset value.
.my-element {
border-image: url('image.jpg');
border-image-slice: 61 58 51 48;
}
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/WETsTwhFSfl30VfyfMTA.png", alt="The image used in the demo with the four slices shown with blue lines", width="800", height="380" %}
With the offset values defined,
you now have 9 sections of the image: 4 corners, 4 edges and a middle section.
The corners are applied to the corners of the element with the border image.
The edges are applied to the edges of that element.
The border-image-repeat
property defines how those edges fill their space and the
border-image-width
property controls the size of the slices.
Lastly, the fill keyword determines whether the middle section, left by the slicing, is used as the element's background image or not.
border-image-repeat
border-image-repeat
is how you instruct CSS how you would like your border image to repeat.
It works the same as background-repeat.
- The initial value is
stretch, which stretches the source image to fill available space where possible. - The
repeatvalue tiles the source image's edges as many times as possible, and may clip the edge regions to achieve this. - The
roundvalue is the same as repeat, but instead of clipping the image edge regions to fit as many as possible, it stretches the image as well as repeating it to achieve a seamless repeat - The
spacevalue is again, the same as repeat, but this value adds space between each edge region to create a seamless pattern.
{% Assessment 'borders' %}
Box Model
Say you have this bit of HTML:
<p>I am a paragraph of text that has a few words in it.</p>
Then you write this CSS for it:
p {
width: 100px;
height: 50px;
padding: 20px;
border: 1px solid;
}
The content would break out of your element and it would be 142px wide, rather than 100px. Why is that? The box model is a core foundation of CSS and understanding how it works, how it is affected by other aspects of CSS and importantly, how you can control it will help you to write more predictable CSS.
A really important thing to remember when writing CSS,
or working on the web as a whole,
is that everything displayed by CSS is a box.
Whether that's a box that uses border-radius to look like a circle, or even just some text:
the key thing to remember is that it's all boxes.
Content and sizing
Boxes have different behavior based on their display value,
their set dimensions, and the content that lives within them.
This content could be even more boxes—generated by child elements—or plain text content.
Either way, this content will affect the size of the box by default.
You can control this by using extrinsic sizing, or, you can continue to let the browser make decisions for you based on the content size, using intrinsic sizing.
Let's quickly look at the difference, using a demo to help us.
The demo has the words, "CSS is awesome" in a box with fixed dimensions and a thick border.
The box has a width, so is extrinsically sized.
It controls the sizing of its child content.
The problem with this though, is that the word "awesome" is too large for the box,
so it overflows outside of the parent box's border box (more on this later in the lesson).
One way to prevent this overflow is to allow the box to be intrinsically sized by either
unsetting the width,
or in this case,
setting the width to be min-content.
The min-content keyword tells the box to only be as wide as the intrinsic minimum width of
its content (the word "awesome").
This allows the box to fit around "CSS is awesome", perfectly.
Let's look at something more complex to see the impact of different sizing on real content:
Toggle intrinsic sizing on and off to see how you can gain more control with extrinsic sizing and let the content have more control with intrinsic sizing. To see the effect that intrinsic and extrinsic sizing has, add a few sentences of content to the card. When this element is using extrinsic sizing, there's a limit of how much content you can add before it overflows out of the element's bounds, but this isn't the case when intrinsic sizing is toggled on.
By default, this element has a set width and height—both 400px.
These dimensions give strict bounds to everything inside the element,
which will be honored unless the content is too large for the box in which case visible overflow
will happen.
You can see this in action by changing the content of the caption,
under the flower picture to something that exceeds the height of the box, which is a few lines of
content.
{% Aside "key-term" %}
When content is too big for the box it is in, we call this overflow.
You can manage how an element handles overflow content, using the overflow property.
{% endAside %}
When you switch to intrinsic sizing, you are letting the browser make decisions for you, based on the box's content size. It's much more difficult for there to be overflow with intrinsic sizing because our box will resize with its content, rather than try to size the content. It's important to remember that this is the default, flexible behavior of a browser. Though extrinsic sizing gives more control on the surface, intrinsic sizing provides the most flexibility, most of the time.
The areas of the box model
Boxes are made up of distinct box model areas that all do a specific job.
You start with content box, which is the area that the content lives in. As you learned before: this content can control the size of its parent, so is usually the most variably sized area.
The padding box surrounds the content box
and is the space created by
the padding property.
Because padding is inside the box, the background of the box will be visible in the space that
it creates.
If our box has overflow rules set, such as overflow: auto or overflow: scroll,
the scrollbars will occupy this space too.
The border box surrounds the padding box and its space is occupied by the border value.
The border box is the bounds of your box and the border edge is the limit of what you can
visually see. The border
property is used to visually frame an element.
The final area, the margin box, is the space around your box,
defined by the margin rule on your box.
Properties such as
outline and
box-shadow
occupy this space too because they are painted on top,
so they don't affect the size of our box.
You could have an outline-width of 200px on our box and everything inside and including the
border box would be exactly the same size.
A useful analogy
The box model is complex to understand, so let's recap what you've learned with an analogy.
In this diagram, you have three photo frames, mounted to a wall, next to each other. The diagram has labels that associate elements of the frame with the box model.
To break this analogy down:
- The content box is the artwork.
- The padding box is the white matte, between the frame and the artwork.
- The border box is the frame, providing a literal border for the artwork.
- The margin box is the space between each frame.
- The shadow occupies the same space as the margin box.
Debugging the box model
Browser DevTools provide a visualisation of a selected box's box model calculations, which can help you understand how the box model works and importantly, how it is affecting the website you're working on.
Go ahead and try this in your own browser:
- Open DevTools
- Select an element
- Show the box model debugger
Controlling the box model
To understand how to control the box model, you first need to understand what happens in your browser.
Every browser applies a user agent stylesheet to HTML documents.
The CSS used varies between each browser,
but they provide sensible defaults to make content easier to read.
They define how elements should look and behave if there's no CSS defined.
It is in the user agent styles where a box's default display is set, too.
For example, if we are in a normal flow, a <div> element's default display value is block,
a <li> has a default display value of list-item,
and a <span> has a default display value of inline.
An inline element has block margin,
but other elements won't respect it.
Use inline-block, and those elements will respect the block margin,
while the element maintains most of the same behaviors it had as an inline element.
A block item will, by default,
fill the available inline space,
whereas a inline and inline-block elements will only be as large as their content.
Alongside an understanding of how user agent styles affect each box,
you also need to understand box-sizing,
which tells our box how to calculate its box size.
By default, all elements have the following user agent style: box-sizing: content-box;.
Having content-box as the value of box-sizing means that when you set dimensions,
such as a width and height,
they will be applied to the content box.
If you then set padding and border,
these values will be added to the content box's size.
{% Assessment 'box-model' %}
The actual width of this box will be 260px.
As the CSS uses the default box-sizing: content-box,
the applied width is the width of the content,
padding and border on both sides are added to that.
So 200px for the content + 40px of padding + 20px of border makes a total visible width of 260px.
You can control this, though,
by making the following modification to use the alternative box model, border-box:
.my-box {
box-sizing: border-box;
width: 200px;
border: 10px solid;
padding: 20px;
}
This alternative box model tells CSS to apply the width to the border box instead of the
content box. This means that our border and padding get pushed in,
and as a result,
when you set .my-box to be 200px wide: it actually renders at 200px wide.
Check out how this works in the following interactive demo.
Notice that when you toggle the box-sizing value
it shows—via a blue background—which CSS is being applied inside our box.
*,
*::before,
*::after {
box-sizing: border-box;
}
This CSS rule selects every element in the document
and every ::before and ::after pseudo element and applies box-sizing: border-box.
This means that every element will now have this alternative box model.
Because the alternative box model can be more predictable, developers often add this rule to resets and normalizers, like this one.
Resources
User agent stylesheets
Color
Color is an important part of any website and in CSS there are many options for color types, functions and treatments.
How do you decide which color type to use? How do you make your colors semi-transparent? In this lesson, you're going to learn which options you have to help you make the right decisions for your project and team.
CSS has various different data types, such as strings and numbers. Color is one of these types and uses other types, such as numbers for its own definitions.
Numeric colors
It is very likely that your first exposure to colors in CSS is via numeric colors. We can work with numerical color values in a few different forms.
Hex colors
h1 {
color: #b71540;
}
Hexadecimal notation (often shortened to hex) is a shorthand syntax for RGB, which assigns a numeric value to red green and blue, which are the three primary colors.
{% Aside %} According to the Web Almanac, hex is the most popular color syntax type. {% endAside %}
The hexadecimal ranges are 0-9 and A-F. When used in a six digit sequence, they are translated to the RGB numerical ranges which are 0-255 which correspond to the red, green, and blue color channels respectively.
You can also define an alpha value with any numerical colors.
An alpha value is a percentage of transparency.
In hex code, you add another two digits to the six digit sequence,
making an eight digit sequence.
For example, to set black in hex code, write #000000.
To add a 50% transparency, change it to #00000080.
Because the hex scale is 0-9 and A-F, the transparency values are probably not quite what you'd expect them to be.
Here are some key, common values added to the black hex code, #000000:
- 0% alpha—which is fully transparent—is 00:
#00000000 - 50% alpha is 80:
#00000080 - 75% alpha is BF:
#000000BF
To convert a two digit hex to a decimal, take the first digit and multiply it by 16 (because hex is base 16), then add the second digit. Using BF as an example for 75% alpha:
- B is equal to 11, which when multiplied by 16 equals 176
- F is equal to 15
- 176 + 15 = 191
- The alpha value is 191—75% of 255
{% Aside %}
You can also write hex codes in a three digit shorthand.
A three digit hex code is a shortcut to an equivalent six digit sequence.
For example, #a4e is identical to #aa44ee.
To add alpha, then #a4e8 would expand to #aa44ee88.
{% endAside %}
RGB (Red, Green, Blue)
h1 {
color: rgb(183, 21, 64);
}
RGB colors are defined with the
rgb() color function,
using either numbers or percentages as parameters.
The numbers need to be within the 0-255 range and the percentages are between 0% and 100%.
RGB works on the 0-255 scale,
so 255 would be equivalent to 100%, and 0 to 0%.
To set black in RGB, define it as rgb(0 0 0),
which is zero red, zero green and zero blue.
Black can also be defined as rgb(0%, 0%, 0%).
White is the exact opposite: rgb(255, 255, 255) or rgb(100%, 100%, 100%).
An alpha is set in rgb() in one of two ways.
Either add a / after the red, green and blue parameters,
or use the rgba() function.
The alpha can be defined with a percentage or a decimal between 0 and 1.
For example, to set a 50% alpha black in modern browsers, write: rgb(0 0 0 / 50%) or rgb(0 0 0 / 0.5).
For wider support, using the rgba() function,
write: rgba(0, 0, 0, 50%) or rgba(0, 0, 0, 0.5).
{% Aside %}
Commas were removed from the rgb() and hsl() notation because newer color functions,
such as lab() and lch() use spaces instead of commas as a delimiter.
This change provides more consistency not just with newer color functions,
but with CSS in general.
For better backwards compatibility,
you can still use commas to define rgb() and hsl().
{% endAside %}
HSL (Hue, Saturation, Lightness)
h1 {
color: hsl(344, 79%, 40%);
}
HSL stands for hue, saturation and lightness. Hue describes the value on the color wheel, from 0 to 360 degrees, starting with red (being both 0 and 360). A hue of 180, or 50% would be in the blue range. It's the origin of the color that we see.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/ob7MTste1Obu9AoLvbKq.svg", alt="A color wheel with labels for degree values in 60 degree increments to help visuals what each angle value represents", width="800", height="507" %}
Saturation is how vibrant the selected hue is.
A fully desaturated color (with a saturation of 0%) will appear grayscale.
And finally, lightness is the parameter which describes the scale from white to black of added light.
A lightness of 100% will always give you white.
Using the hsl() color function,
you define a true black by writing hsl(0 0% 0%), or even hsl(0deg 0% 0%).
This is because the hue parameter defines the degree on the color wheel,
which if you use the number type, is 0-360.
You can also use the angle type, which is (0deg) or (0turn).
Both saturation and lightness are defined with percentages.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/RhVxWSZ6bH35eBdN6Prj.svg", alt="The HSL color function broken down visually. The hue uses the color wheel. The saturation shows grey blending into teal. The lightness shows black into white.", width="800", height="478" %}
{% Aside %} The angle type in CSS is great for defining hue because it represents the angle of the color wheel really well. This type accepts degrees, turns, radians and gradians. {% BrowserCompat 'css.types.angle' %} {% endAside %}
Alpha is defined in hsl(),
in the same way as rgb() by adding a / after the hue, saturation and lightness parameters or by using the
hsla() function.
The alpha can be defined with a percentage or a decimal between 0 and 1.
For example, to set a 50% alpha black, use: hsl(0 0% 0% / 50%) or hsl(0 0% 0% / 0.5).
Using the hsla() function, write: hsla(0, 0%, 0%, 50%) or hsla(0, 0%, 0%, 0.5).
{% Aside %} There are some newer color types coming to CSS. These include lab() and lch(), which allow a far wider range of color to be specified than is possible in RGB. {% endAside %}
Color Keywords
There are 148 named colors in CSS. These are plain English names such as purple, tomato and goldenrod. Some of the most popular names, according to the Web Almanac, are black, white, red, blue and gray. Our favorites include goldenrod, aliceblue, and hotpink.
Aside from standard colors, there are also special keywords available:
transparentis a fully transparent color. It is also the initial value ofbackground-colorcurrentColoris the contextual computed dynamic value of thecolorproperty. If you have a text color ofredand then set theborder-colorto becurrentColor, it will also be red. If the element that you definecurrentColoron doesn't have a value forcolordefined,currentColorwill be computed by the cascade instead
{% Aside %}
System keywords are colors that are defined by your operating system theme.
Some examples of these colors are Background,
which is the desktop background color or Highlight,
which is the highlight color of selected items. These are just two of
many options.
All color keywords are case-insensitive, however you will often see system colors with capitalization to differentiate them from standard color keywords. {% endAside %}
Where to use color in CSS rules
If a CSS property accepts the
<color> data type as a value,
it will accept any of the above methods of expressing color.
For styling text, use the color, text-shadow and text-decoration-color properties
which all accept color as the value or color as part of the value.
For backgrounds, you can set a color as the value for background or background-color.
Colors can also be used in gradients, such as linear-gradient.
Gradients are a type of image that can be programmatically defined in CSS.
Gradients accept two or more colors in any combination of color format, such as hex, rgb or hsl.
{% Aside %} There's lots to learn with gradients so we wrote a whole lesson on how to use them. {% endAside %}
Finally, border-color, and outline-color set the color for borders and outlines on your boxes.
The box-shadow property also accepts color as one of the values.
{% Assessment 'color' %}
Resources
- A handy demo showing how you can use angles with HSL
- A comprehensive guide on color
- A comprehensive guide on wide gamut color
- [video] An explainer on how to read hex codes
- How hexadecimal codes work
Conclusion and next steps
Congratulations! You've made it to the end! By now, you have learned a lot about CSS, and hopefully you feel more comfortable working with it than you did before.
You might be wondering where you can go from here. One of the best ways to learn is to learn by doing: try out your new CSS skills by building a website of your own. To continue to build your CSS skillset, here are some additional resources to explore:
Additional resources
- CSS Documentation on MDN
- Listen to the CSS Podcast
- CSS articles on web.dev
- How To Learn CSS - Smashing Magazine
- CSS-Tricks
- Layout Land - Youtube
Brought to you by
{% include 'partials/authors-stack.njk' %}
Filters
Say you need to build an element that's got a slightly opaque, frosted glass effect that sits over the top of an image. The text needs to be live text and not an image. How do you do that?
A combination of CSS filters and the backdrop-filter
allow us to apply effects and blur what's needed in real time.
Blur and opacity are two of many available filters,
so let's have a quick run through what they all do and how to use them.
{% Aside %}
Take care when placing text over images,
that the text is still readable should the filter effect not be supported in a user's browser.
For example, at the moment
backdrop-filter
is not supported in Firefox,
and so you should check that Firefox users aren't left with text they cannot easily read:
{% BrowserCompat 'css.properties.backdrop-filter' %}
{% endAside %}
The filter property
{% BrowserCompat 'css.properties.filter' %}
You can apply one or many of the following filters as a value for
filter.
If you incorrectly apply a filter,
the rest of the filters defined for filter will not work.
blur
This applies a gaussian blur and the only argument you can pass is a radius,
which is
how much blur is applied.
This needs to be a length unit, like 10px. Percentages are not accepted.
.my-element {
filter: blur(0.2em);
}
brightness
{% BrowserCompat 'css.types.filter-function.brightness' %}
To increase or decrease the brightness of an element, use the brightness function. The brightness value is expressed as a percentage with the unchanged image being expressed as a value of 100%. A value of 0% turns the image completely black, therefore values between 0% and 100% make the image less bright. Use values over 100% to increase the brightness.
.my-element {
filter: brightness(80%);
}
{% Aside %}
You can also use decimal values,
instead of percentage values in filters like brightness.
To set 80% brightness with a decimal, write 0.8.
{% endAside %}
contrast
{% BrowserCompat 'css.types.filter-function.contrast' %}
Set a value between 0% and 100% to decrease or increase the contrast, respectively.
.my-element {
filter: contrast(160%);
}
grayscale
{% BrowserCompat 'css.types.filter-function.grayscale' %}
You can apply a completely grayscale effect by using 1 as a value for grayscale(),
or 0 to have a completely saturated element.
You can also use percentage or decimal values to only apply a partial grayscale effect.
If you pass no arguments, the element will be completely grayscale.
If you pass a value greater than 100%, it will be capped at 100%.
.my-element {
filter: grayscale(80%);
}
invert
{% BrowserCompat 'css.types.filter-function.invert' %}
Just like grayscale,
you can pass 1 or 0 to the invert() function to turn it on or off.
When it is on, the element's colors are completely inverted.
You can also use percentage or decimal values to only apply a partial inversion of colors.
If you don't pass any arguments into the invert() function,
the element will be completely inverted.
.my-element {
filter: invert(1);
}
opacity
{% BrowserCompat 'css.types.filter-function.opacity' %}
The opacity() filter works just like the opacity property,
where you can pass a number or percentage to increase or reduce opacity.
If you pass no arguments, the element is fully visible.
.my-element {
filter: opacity(0.3);
}
saturate
{% BrowserCompat 'css.types.filter-function.saturate' %}
The saturate filter is very similar to the brightness filter and accepts the same argument:
number or percentage.
Instead of increasing or decreasing the brightness effect,
saturate increases or decreases color saturation.
.my-element {
filter: saturate(155%);
}
sepia
{% BrowserCompat 'css.types.filter-function.sepia' %}
You can add a sepia tone effect with this filter that works like grayscale().
The sepia tone is a photographic printing technique that converts black tones to brown tones to warm them up.
You can pass a number or percentage as the argument for sepia()
which increases or decreases the effect.
Passing no arguments adds a full sepia effect (equivalent to sepia(100%)).
.my-element {
filter: sepia(70%);
}
hue-rotate
{% BrowserCompat 'css.types.filter-function.hue-rotate' %}
You learned about how the hue in hsl references a rotation of the color wheel in the
colors lesson and this filter works in a similar way.
If you pass an angle, such as degrees or turns,
it shifts the hue of all the element's colors,
changing the part of the color wheel it references. If you pass no argument, it does nothing.
.my-element {
filter: hue-rotate(120deg);
}
drop-shadow
{% BrowserCompat 'css.types.filter-function.drop-shadow' %}
You can apply a curve-hugging drop shadow like you would in a design tool,
such as Photoshop with
drop-shadow.
This shadow is applied to an alpha mask which makes it very useful for adding a shadow to a cutout image.
The drop-shadow filter takes a shadow parameter which contains space separated offset-x, offset-y, blur and color values.
It is almost identical to box-shadow,
but the inset keyword and spread value are not supported.
.my-element {
filter: drop-shadow(5px 5px 10px orange);
}
Learn more about the different types of shadows in the shadows module.
url
{% BrowserCompat 'svg.elements.filter' %}
The url filter allows you to apply an SVG filter from a linked SVG element or file.
You can
read more about SVG filters here
Backdrop filter
{% BrowserCompat 'css.properties.backdrop-filter' %}
The backdrop-filter
property accepts all of the same filter function values as filter.
The difference between backdrop-filter and filter
is that the backdrop-filter property only applies the filters to the background,
where the filter property applies it to the whole element.
The example right at the start of this lesson is the perfect example, because you don't want the text to be blurred and ideally you don't want to have to add extra HTML elements. Being able to apply filters only to the backdrop enables that.
{% Assessment 'filters' %}
Flexbox
A design pattern that can be tricky in responsive design is a sidebar that sits inline with some content. Where there is viewport space, this pattern works great, but where space is condensed, that rigid layout can become problematic.
The Flexible Box Layout Model (flexbox) is a layout model designed for one-dimensional content. It excels at taking a bunch of items which have different sizes, and returning the best layout for those items.
This is the ideal layout model for this sidebar pattern. Flexbox not only helps lay the sidebar and content out inline, but where there's not enough space remaining, the sidebar will break onto a new line. Instead of setting rigid dimensions for the browser to follow, with flexbox, you can instead provide flexible boundaries to hint how the content could display.
What can you do with a flex layout?
Flex layouts have the following features, which you will be able to explore in this guide.
- They can display as a row, or a column.
- They respect the writing mode of the document.
- They are single line by default, but can be asked to wrap onto multiple lines.
- Items in the layout can be visually reordered, away from their order in the DOM.
- Space can be distributed inside the items, so they become bigger and smaller according to the space available in their parent.
- Space can be distributed around the items and flex lines in a wrapped layout, using the Box Alignment properties.
- The items themselves can be aligned on the cross axis.
The main axis and the cross axis
The key to understanding flexbox is to understand the concept of a main axis and a cross axis.
The main axis is the one set by your flex-direction property.
If that is row your main axis is along the row,
if it is column your main axis is along the column.
Flex items move as a group on the main axis. Remember: we've got a bunch of things and we are trying to get the best layout for them as a group.
The cross axis runs in the other direction to the main axis,
so if flex-direction is row the cross axis runs along the column.
You can do two things on the cross axis.
You can move the items individually or as a group, so they align against each other and the flex
container. Also, if you have wrapped flex lines,
you can treat those lines as a group to control how space is assigned to those lines.
You will see how this all works in practice throughout this guide,
for now just keep in mind that the main axis follows your flex-direction.
Creating a flex container
Let's see how flexbox behaves by taking a group of different sized items and using flexbox to lay them out.
<div class="container" id="container">
<div>One</div>
<div>Item two</div>
<div>The item we will refer to as three</div>
</div>
To use flexbox you need to declare that you want to use a flex formatting context and not regular
block and inline layout.
Do this by changing the value of the display property to flex.
.container {
display: flex;
}
As you learned in the layout guide this will give you a block-level box, with flex item children. The flex items immediately start exhibiting some flexbox behavior, using their initial values.
{% Aside %}
All CSS properties have initial values which control how they behave "out of the box"
when you haven't applied any CSS to change that initial behavior.
The children of our flex container become flex items as soon as their parent gets
display: flex, so these initial values mean that we start seeing some flexbox behavior.
{% endAside %}
The initial values mean that:
- Items display as a row.
- They do not wrap.
- They do not grow to fill the container.
- They line up at the start of the container.
Controlling the direction of items
Even though you haven't added a flex-direction property yet,
the items display as a row because the initial value of flex-direction is row.
If you want a row then you don't need to add the property.
To change the direction, add the property and one of the four values:
row: the items lay out as a row.row-reverse:the items lay out as a row from the end of the flex container.column: the items lay out as a column.column-reverse: the items lay out as a column from the end of the flex container.
You can try out all of the values using our group of items in the demo below.
Reversing the flow of items and accessibility
You should be cautious when using any properties that reorder the visual display
away from how things are ordered in the HTML document,
as it can negatively impact accessibility.
The row-reverse and column-reverse values are a good example of this.
The reordering only happens for the visual order, not the logical order.
This is important to understand as the logical order is the order that a screen reader will read out
the content,
and anyone navigating using the keyboard will follow.
You can see in the following video how in a reversed column layout, tabbing between links becomes disconnected as the keyboard navigation follows the DOM not the visual display.
{% Video src="video/VbAJIREinuYvovrBzzvEyZOpw5w1/IgpaIRZd7kOq8sd46eaR.mp4", autoplay=true, controls=true %}
Anything which can change the order of items in flexbox or grid can cause this problem. Therefore any reordering should include thorough testing to check that it will not make your site hard to use for some people.
For more information see:
Writing modes and direction
Flex items lay out as a row by default. A row runs in the direction that sentences flow in your writing mode and script direction. This means that if you are working in Arabic, which has a right-to-left (rtl) script direction, the items will line up on the right. Tab order would also begin on the right as this is the way sentences are read in Arabic.
If you are working with a vertical writing mode,
like some Japanese typefaces, then a row will run vertically, from top to bottom.
Try changing the flex-direction in this demo which is using a vertical writing mode.
Therefore the way flex items behave by default is linked to the writing mode of the document. Most tutorials are written using English, or another horizontal, left to right writing mode. This would make it easy to assume that flex items line up on the left, and run horizontally.
With the main and cross axis plus the writing mode to consider, the fact that we talk about start and end rather than top, bottom, left, and right in flexbox might be easier to understand. Each axis has a start and an end. The start of the main axis is referred to as main-start. So our flex items initially line up from main-start. The end of that axis is main-end. The start of the cross axis is cross-start and the end cross-end.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/uSH4TxRv8KNQDTK7Vn8h.svg", alt="A labelled diagram of the above terms", width="800", height="382" %}
Wrapping flex items
The initial value of the flex-wrap property is nowrap.
This means that if there is not enough space in the container the items will overflow.
Items displaying using the initial values will shrink as small as they can,
down to the min-content size before overflow happens.
To cause the items to wrap add flex-wrap: wrap to the flex container.
.container {
display: flex;
flex-wrap: wrap;
}
When a flex container wraps it creates multiple flex lines. In terms of space distribution, each line acts like a new flex container. Therefore if you are wrapping rows, it is not possible to get something in row 2 to line up with something above it in row 1. This is what is meant by flexbox being one-dimensional. You can control alignment in one axis, a row or a column, not both together as we can do in grid.
The flex-flow shorthand
You can set the flex-direction and flex-wrap properties using the shorthand flex-flow.
For example, to set flex-direction to column and allow items to wrap:
.container {
display: flex;
flex-flow: column wrap;
}
Controlling space inside flex items
Assuming our container has more space than is needed to display the items,
the items line up at the start and do not grow to fill the space.
They stop growing at their max-content size.
This is because the initial value of the flex- properties is:
flex-grow: 0: items do not grow.flex-shrink: 1: items can shrink smaller than theirflex-basis.flex-basis: auto: items have a base size ofauto.
This can be represented by a keyword value of flex: initial.
The flex shorthand property,
or the longhands of flex-grow, flex-shrink and flex-basis are applied to the children of the
flex container.
To cause the items to grow,
while allowing large items to have more space than small ones use flex:auto.
You can try this using the demo above.
This sets the properties to:
flex-grow: 1: items can grow larger than theirflex-basis.flex-shrink: 1: items can shrink smaller than theirflex-basis.flex-basis: auto: items have a base size ofauto.
Using flex: auto will mean that items end up different sizes,
as the space that is shared between the items is shared out after each item is laid out as
max-content size.
So a large item will gain more space.
To force all of the items to be a consistent size and ignore the size of the content change
flex:auto to flex: 1 in the demo.
This unpacks to:
flex-grow: 1: items can grow larger than theirflex-basis.flex-shrink: 1: items can shrink smaller than theirflex-basis.flex-basis: 0: items have a base size of0.
Using flex: 1 says that all items have zero size,
therefore all of the space in the flex container is available to be distributed.
As all items have a flex-grow factor of 1 they all grow equally and the space is shared equally.
{% Aside %}
There is also a value of flex: none,
which will give you inflexible flex items that do not grow or shrink.
This might be useful if you are purely using flexbox to access the alignment properties but don't
want any flexible behavior.
{% endAside %}
Allowing items to grow at different rates
You don't have to give all items a flex-grow factor of 1.
You could give your flex items different flex-grow factors.
In the demo below the first item has flex: 1, the second flex: 2 and third flex: 3.
As these items grow from 0 the available space in the flex container is shared into six.
One part is given to the first item,
two parts to the second,
three parts to the third.
You can do the same thing from a flex-basis of auto, though you will need to specify the three
values.
The first value being flex-grow,
the second flex-shrink,
and the third flex-basis.
.item1 {
flex: 1 1 auto;
}
.item2 {
flex: 2 1 auto;
}
This is a less common use case as the reason to use a flex-basis of auto
is to allow the browser to figure out space distribution.
If you wanted to cause an item to grow a little more than the algorithm decides however it might be
useful.
Reordering flex items
Items in your flex container can be reordered using the order property.
This property allows the ordering of items in ordinal groups.
Items are laid out in the direction dictated by flex-direction,
lowest values first.
If more than one item has the same value it will be displayed with the other items with that value.
The example below demonstrates this ordering.
{% Aside 'warning' %}
Using order has the same problems as the row-reverse and column-reverse
values of flex-direction.
It would be very easy to create a disconnected experience for some users.
Do not use order because you are fixing things being out of order in the document.
If the items logically should be in a different order, change your HTML!
{% endAside %}
{% Assessment 'flex' %}
Flexbox alignment overview
Flexbox brought with it a set of properties for aligning items and distributing space between items. These properties were so useful they have since been moved into their own specification, you'll encounter them in Grid Layout too. Here you can find out how they work when you are using flexbox.
The set of properties can be placed into two groups. Properties for space distribution, and properties for alignment. The properties which distribute space are:
justify-content: space distribution on the main axis.align-content: space distribution on the cross axis.place-content: a shorthand for setting both of the above properties.
The properties used for alignment in flexbox:
align-self: aligns a single item on the cross axis.align-items: aligns all of the items as a group on the cross axis.
If you are working on the main axis then the properties begin with justify-.
On the cross axis they begin with align-.
Distributing space on the main axis
With the HTML used earlier, the flex items laid out as a row, there is space on the main axis.
The items are not big enough to completely fill the flex container.
The items line up at the start of the flex container because the initial value of justify-content
is flex-start.
The items line up at the start and any extra space is at the end.
Add the justify-content property to the flex container,
give it a value of flex-end,
and the items line up at the end of the container and the spare space is placed at the start.
.container {
display: flex;
justify-content: flex-end;
}
You can also distribute the space between the items with justify-content: space-between.
Try some of the values in the demo, and see MDN for the full set of possible values.
{% Aside %}
For the justify-content property to do anything you have to have spare space in your container on
the main axis.
If your items fill the axis then there is no space to share out so the property won't do anything.
{% endAside %}
With flex-direction: column
If you have changed your flex-direction to column then justify-content will work on
the column.
To have spare space in your container when working as a column you need to give your container a
height or block-size.
Otherwise you won't have spare space to distribute.
Try the different values, this time with a flexbox column layout.
Distributing space between flex lines
With a wrapped flex container you might have space to distribute on the cross axis.
In this case you can use the align-content property with the same values as justify-content.
Unlike justify-content which aligns items to flex-start by default,
the initial value of align-content is stretch.
Add the property align-content to the flex container to change that default behavior.
.container {
align-content: center;
}
Try this out in the demo.
The example has wrapped lines of flex items,
and the container has a block-size in order that we have some spare space.
The place-content shorthand
To set both justify-content and align-content you can use place-content with one
or two values.
A single value will be used for both axes,
if you specify both the first is used for align-content and the second for justify-content.
.container {
place-content: space-between;
/* sets both to space-between */
}
.container {
place-content: center flex-end;
/* wrapped lines on the cross axis are centered,
on the main axis items are aligned to the end of the flex container */
}
Aligning items on the cross-axis
On the cross axis you can also align your items within the flex line using align-items
and align-self.
The space available for this alignment will depend on the height of the flex container,
or flex line in the case of a wrapped set of items.
The initial value of align-self is stretch,
which is why flex items in a row stretch to the height of the tallest item by default.
To change this, add the align-self property to any of your flex items.
.container {
display: flex;
}
.item1 {
align-self: flex-start;
}
Use any of the following values to align the item:
flex-startflex-endcenterstretchbaseline
See the full list of values on MDN.
The next demo has a single line of flex items with flex-direction: row.
The last item defines the height of the flex container.
The first item has the align-self property with a value of flex-start.
Try changing the value on that property to see how it moves within its space on the cross axis.
The align-self property is applied to individual items.
The align-items property can be applied to the flex container
to set all of the individual align-self properties as a group.
.container {
display: flex;
align-items: flex-start;
}
In this next demo try changing the value of align-items to align all of the items on the cross
axis as a group.
Why is there no justify-self in flexbox?
Flex items act as a group on the main axis. So there is no concept of splitting an individual item out of that group.
In grid layout the justify-self and justify-items properties work on the inline axis
to do alignment of items on that axis within their grid area.
Due to the way that flex layouts treat items as a group,
these properties are not implemented in a flex context.
It is worth knowing that flexbox does work very nicely with auto margins.
If you come across a need to split off one item from a group,
or separate the group into two groups you can apply a margin to do this.
In the example below the last item has a left margin of auto.
The auto margin absorbs all space in the direction it is applied.
This means that it pushes the item over to the right, thus splitting the groups.
How to center an item vertically and horizontally
The alignment properties can be used to center an item inside another box.
The justify-content property aligns the item on the main axis,
which is row. The align-items property on the cross axis.
.container {
width: 400px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
}
{% Aside %} In the future we may be able to do this alignment without needing to make the parent a flex container. The alignment properties are specified for block and inline layout. At present no browser has implemented these. However, switching into a flex formatting context gives you access to the properties. If you need to align something it's a great way to do it. {% endAside %}
{% Assessment 'conclusion' %}
Resources
- MDN CSS Flexible Box Layout includes a series of detailed guides with examples.
- CSS Tricks Guide to Flexbox
- What Happens When You Create a Flexbox Flex Container
- Everything You Need To Know About Alignment In Flexbox
- How Big Is That Flexible Box?
- Use Cases For Flexbox
- Inspect and debug CSS Flexbox layouts in Chrome DevTools
Focus
On your webpage, you click a link that skips the user to the main content of the website. These are often referred to as skip links, or anchor links. When that link is activated by a keyboard, using the tab and enter keys, the main content container has a focus ring around it. Why is that?
This is because the <main> has a tabindex="-1" attribute value,
which means it can be programmatically focused.
When the <main> is targeted—because the #main-content
in the browser URL bar matches the id—it receives programmatic focus.
It's tempting to remove the focus styles in these situations,
but handling focus appropriately and with care helps to create a good,
accessible, user experience.
It can also be a great place to add some interest to interactions.
Why is focus important?
As a web developer, it's your job to make a website accessible and inclusive to all. Creating accessible focus states with CSS is a part of this responsibility.
Focus styles assist people who use a device such as a keyboard or a switch control to navigate and interact with a website. If an element receives focus and there is no visual indication, a user may lose track of what is in focus. This can create navigation issues and result in unwanted behaviour if, say, the wrong link is followed.
{% Aside %} Learn more about the importance of focus for accessibility in Learn Accessibility: Focus, and more information on how to manage focus in HTML in Learn HTML: Focus. {% endAside%}
How elements get focus
Certain elements are automatically focusable;
these are elements that accept interaction and input, such as <a>,
<button>, <input> and <select>.
In short, all form elements, buttons and links.
You can typically navigate a website's focusable elements using the tab key to move forward on the page, and shift + tab to move backward.
There is also a HTML attribute called tabindex which allows you to change the tabbing index—which is
order in which elements are focused—every time someone presses their tab key,
or focus is shifted with a hash change in the URL or by a JavaScript event.
If tabindex on a HTML element is set to 0,
it can receive focus via the tab key and it will honour the global tab index,
which is defined by the document source order.
If you set tabindex to -1, it can only receive focus programmatically,
which means only when a JavaScript event happens
or a hash change (matching the element's id in the URL) occurs.
If you set tabindex to be anything higher than 0,
it will be removed from the global tab index,
defined by document source order.
Tabbing order will now be defined by the value of tabindex,
so an element with tabindex="1" will receive focus before an element with tabindex="2", for example.
{% Aside 'warning' %}
Honoring document source order is really important,
and focus order should only be changed if you absolutely have to change it.
This applies both when setting tabindex and changing visual order with CSS layout,
such as flexbox and grid.
Anything that creates unpredictable focus on the web
can create an inaccessible experience for the user.
{% endAside %}
Styling focus
The default browser behavior when an element receives focus is to present a focus ring. This focus ring varies between both browser and operating systems.
{% Video src="video/VbAJIREinuYvovrBzzvEyZOpw5w1/zVu8FoqteYCP1sTj3uCm.mp4", controls="true", autoplay="true" %}
This behavior can be changed with CSS,
using the :focus, :focus-within and :focus-visible
pseudo-classes that you learned about in the
pseudo-classes lesson.
It is important to set a focus style which has contrast with the default style of an element.
For example, a common approach is to utilize the outline property.
a:focus {
outline: 2px solid slateblue;
}
The outline property could appear too close to the text of a link,
but the outline-offset property can help with that,
as it adds extra visual padding without affecting the geometric size that the element fills.
A positive number value for outline-offset will push the outline outwards,
a negative value will pull the outline inwards.
Currently in some browsers,
if you have a border-radius set on your element and use outline,
it won't match—the outline will have sharp corners.
Due to this,
it is tempting to use a box-shadow with a small blur radius because box-shadow clips to the shape,
honouring border-radius,
but this style will not show in Windows High Contrast Mode.
This is because Windows High Contrast Mode doesn't apply shadows,
and mostly ignores background images to favor the user's preferred settings.
{% Video src="video/VbAJIREinuYvovrBzzvEyZOpw5w1/MqMoKpLJ2rF5RRkn5jEO.mp4" %}
In summary
Creating a focus state that has contrast with an element's default state is incredibly important. The default browser styles do this already for you, but if you want to change this behaviour, remember the following:
- Avoid using
outline: noneon an element that can receive keyboard focus. - Avoid replacing
outlinestyles withbox-shadow. as they don't show up in Windows High Contrast Mode. - Only set a positive value for
tabindexon an HTML element if you absolutely have to. - Make sure the focus state is very clear vs the default state.
{% Assessment 'focus' %}
Functions
So far in this course, you've been exposed to several CSS functions.
In the grid module,
you were introduced to minmax() and fit-content(),
which help you size elements.
In the color module,
you were introduced to rgb(), and hsl(), which help you define colors.
Like many other programming languages, CSS has a lot of built-in functions that you can access whenever you need them.
Every CSS function has a specific purpose, in this lesson, you'll get a high-level overview, giving you a much better understanding of where and how to use them.
What is a function?
A function is a named, self-contained piece of code that completes a specific task. A function is named so you can call it within your code and you can pass data into the function. This is known as passing arguments.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/OCUXCuKLxHwZYFIIErUd.svg", alt="A diagram of a function as described above", width="800", height="311" %}
A lot CSS functions are pure functions,
which means that if you pass the same arguments into them,
they will always give you the same result back,
regardless of what is happening in the rest of your code.
These functions will often re-compute as values change in your CSS,
similar to other elements in the language,
such as computed cascaded values like currentColor.
In CSS, you can only use the provided functions, rather than write your own, but functions can be nested within each other in some contexts, giving them more flexibility. We'll cover that in more detail, later in this module.
Functional selectors
.post :is(h1, h2, h3) {
line-height: 1.2;
}
You learned about functional selectors in the
pseudo-classes module
which detailed functions like
:is() and
:not().
The arguments passed into these functions are CSS selectors,
which are then evaluated.
If there is a match with elements,
the rest of the CSS rule will be applied to them.
Custom properties and var()
:root {
--base-color: #ff00ff;
}
.my-element {
background: var(--base-color);
}
A custom property is a variable which allows you to tokenize values in your CSS code.
Custom properties are also affected by the cascade
which means they can be contextually manipulated or redefined.
A custom property must be prefixed with two dashes (--) and are case sensitive.
The var()
function takes one required argument:
the custom property that you are trying to return as a value.
In the above snippet, the var() function has --base-color passed as an argument.
If --base-color is defined, then var() will return the value.
.my-element {
background: var(--base-color, hotpink);
}
You can also pass a fallback declaration value into the var() function.
This means that if --base-color can't be found,
the passed declaration will be used instead, which in this sample's case is the hotpink color.
Functions that return a value
The var() function is just one of the CSS functions that return a value.
Functions like
attr() and
url() follow a similar structure to var()—
you pass one or more arguments and use them on the right side of your CSS declaration.
a::after {
content: attr(href);
}
The attr() function here
is taking the content of the <a> element's href attribute
and setting it as the content of the ::after pseudo-element.
If the value of the <a> element's href attribute was to change,
this would automatically be reflected in this content attribute.
.my-element {
background-image: url('/path/to/image.jpg');
}
The url() function takes a string URL and is used to load images, fonts and content.
If a valid URL isn't passed in or the resource that the URL points to can't be found,
nothing will be returned by the url() function.
Color functions
You learned all about color functions in the color module. If you haven't read that one yet, it is strongly recommended that you do.
Some available color functions in CSS are
rgb(), rgba(), hsl(), hsla(), hwb(), lab() and lch().
All of these have a similar form where configuration arguments are passed in and a color is returned back.
Mathematical expressions
Like many other programming languages, CSS provides useful mathematical functions to assist with various types of calculation.
calc()
{% BrowserCompat 'css.types.calc' %}
The calc()
function takes a single mathematical expression as its parameter.
This mathematical expression can be a mix of types,
such as length, number, angle and frequency. Units can be mixed too.
.my-element {
width: calc(100% - 2rem);
}
In this example, the calc() function is being used to size an element's width
as 100% of its containing parent element,
then removing 2rem off that computed value.
:root {
--root-height: 5rem;
}
.my-element {
width: calc(calc(10% + 2rem) * 2);
height: calc(var(--root-height) * 3);
}
The calc() function can be nested inside another calc() function.
You can also pass custom properties in a var() function as part of an expression.
min() and max()
{% BrowserCompat 'css.types.min' %}
The min()
function returns the smallest computed value of the one or more passed arguments.
The max()
function does the opposite: get the largest value of the one or more passed arguments.
.my-element {
width: min(20vw, 30rem);
height: max(20vh, 20rem);
}
In this example,
the width should be the smallest value between 20vw
—which is 20% of the viewport width—and 30rem.
The height should be the largest value between 20vh
—which is 20% of the viewport height—and 20rem.
{% Aside %}
We cover units like vw and vh in the sizing units module.
{% endAside %}
clamp()
{% BrowserCompat 'css.types.clamp' %}
The clamp()
function takes three arguments: the minimum size,
the ideal size and the maximum.
h1 {
font-size: clamp(2rem, 1rem + 3vw, 3rem);
}
In this example, the font-size will be fluid based on the width of the viewport.
The vw unit is added to a rem unit to assist with screen zooming,
because regardless of zoom level a vw unit will be the same size.
Multiplying by a rem unit—based on the root font size—
provides the clamp() function with a relative calculation point.
You can learn more about the min(), max(), and clamp() functions in
this article.
Shapes
The
clip-path,
offset-path and
shape-outside
CSS properties use shapes to visually clip your box or provide a shape for content to flow around.
There are shape functions that can be used with both of these properties.
Simple shapes such as
circle(),
ellipse() and
inset()
take configuration arguments to size them.
More complex shapes, such as
polygon()
take comma separated pairs of X and Y axis values to create custom shapes.
.circle {
clip-path: circle(50%);
}
.polygon {
clip-path: polygon(0% 0%, 100% 0%, 100% 75%, 75% 75%, 75% 100%, 50% 75%, 0% 75%);
}
Like polygon(), there is also a path() function which takes an SVG fill rule as an argument.
This allows for highly complex shapes that can be drawn in a graphics tool
such as Illustrator or Inkscape and then copied into your CSS.
Transforms
Lastly in this overview of CSS functions are the transform functions,
which skew, resize and even change the depth of an element.
All of the following functions are used with the transform property.
Rotation
{% BrowserCompat 'css.types.transform-function.rotate' %}
You can rotate an element using the
rotate()
function, which will rotate an element on its center axis.
You can also use the
rotateX(),
rotateY() and
rotateZ()
functions to rotate an element on a specific axis instead.
You can pass degree, turn and radian units to determine the level of rotation.
.my-element {
transform: rotateX(10deg) rotateY(10deg) rotateZ(10deg);
}
The rotate3d()
function takes four arguments.
{% BrowserCompat 'css.types.transform-function.rotate3d' %}
The first 3 arguments are numbers, which define the X, Y and Z coordinates. The fourth argument is the rotation which, like the other rotation functions, accepts degrees, angle and turns.
.my-element {
transform: rotate3d(1, 1, 1, 10deg);
}
Scale
{% BrowserCompat 'css.types.transform-function.scale' %}
You can change the scaling of an element with transform and the
scale() function.
The function accepts one or two numbers as a value which determine a positive or negative scaling.
If you only define one number argument,
both the X and Y axis will be scaled the same and defining both is a shorthand for X and Y.
Just like rotate(),
there are
scaleX(),
scaleY() and
scaleZ()
functions to scale an element on a specific axis instead.
.my-element {
transform: scaleX(1.2) scaleY(1.2);
}
Also like the rotate function, there is a
scale3d() function.
This is similar to scale(), but it takes three arguments: the X, Y and Z scale factor.
Translate
{% BrowserCompat 'css.types.transform-function.translate' %}
The translate()
functions move an element while it maintains its position in the document flow.
They accept length and percentage values as arguments.
The translate() function translates an element along the X axis if one argument is defined,
and translates an element along the X and Y axis if both arguments are defined.
.my-element {
transform: translatex(40px) translatey(25px);
}
You can—just like with other transform functions—use specific functions for a specific axis,
using
translateX,
translateY and
translateZ.
You can also use
translate3d
which allows you to define the X, Y and Z translation in one function.
Skewing
{% BrowserCompat 'css.types.transform-function.skew' %}
You can skew an element, using the
skew()
functions which accept angles as arguments.
The skew() function works in a very similar way to translate().
If you only define one argument, it will only affect the X axis and if you define both,
it will affect the X and Y axis.
You can also use
skewX and
skewY to affect each axis independently.
.my-element {
transform: skew(10deg);
}
Perspective
{% BrowserCompat 'css.properties.perspective' %}
Lastly, you can use the
perspective property
—which is part of the transform family of properties—to alter the distance between the user and the Z plane.
This gives the feeling of distance and can be used to create a depth of field in your designs.
This example by David Desandro, from their very useful article, shows how it can be used,
along with perspective-origin-x and perspective-origin-y properties to create truly 3D experiences.
Animation functions, gradients and filters
CSS also provides functions that help you animate elements, apply gradients to them and use graphical filters to manipulate how they look. To keep this module as concise as possible, they are covered in the linked modules. They all follow a similar structure to the functions demonstrated in this module.
{% Assessment 'functions' %}
Gradients
Imagine you've got a site to build and at the top, there's an intro with a heading, summary and a button. The designer has handed over a design which has a purple background for this intro. The only problem is the background features two shades of purple as a gradient. How do you do this?
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/0T5kGhQWJTLUclxSWipH.svg", alt="A dark to light purple gradient background with heading, paragraph and link", width="800", height="411" %}
You might initially think that you're going to need to export an image from your design tool for this,
but you can use a
linear-gradient
instead.
A gradient is an image and can be used anywhere images can be used, but it's created with CSS and is made up with colors, numbers and angles. CSS gradients allow you to create anything from a smooth gradient between two colors, right up to impressive artwork by mixing and repeating multiple gradients.
Linear gradient
{% BrowserCompat 'css.types.image.gradient.linear-gradient' %}
The linear-gradient()
function generates an image of two or more colors, progressively.
It takes multiple arguments, but in its simplest configuration,
you can pass some colors like this and it will automatically split them evenly, while blending them.
.my-element {
background: linear-gradient(black, white);
}
You can also pass an angle or keywords that represent an angle.
If you choose to use keywords, specify a direction after the to keyword.
This means if you want a gradient that is black and white,
that runs from left (black) to right (white),
you would specify the angle as to right as the first argument.
.my-element {
background: linear-gradient(to right, black, white);
}
A color stop value defined where a color stops and mixes with its neighbors. For a gradient starting with a dark shade of red running at a 45deg angle, at 30% of the size of the gradient changing to a lighter red: it looks like this.
.my-element {
background: linear-gradient(45deg, darkred 30%, crimson);
}
You can add as many colors and color stops as you like in a linear-gradient(),
and you can layer gradients on top of each other by separating each gradient with a comma.
Radial gradient
{% BrowserCompat 'css.types.image.gradient.radial-gradient' %}
To create a gradient that radiates in a circular fashion, the
radial-gradient()
function steps in to help.
It's similar to linear-gradient(),
but instead of specifying an angle, you optionally specify a position and ending shape.
If you just specify colors, the radial-gradient() will auto-select the position as center
and select either a circle or ellipse, depending on the size of the box.
.my-element {
background: radial-gradient(white, black);
}
The gradient's position is similar to background-position using keywords and/or number values.
The size of the radial gradient determines the size of the gradient's ending shape
(circle or ellipse) and by default will be farthest-corner,
which means it exactly meets the farthest corner of the box from the center.
You can also use the following keywords:
closest-cornerwill meet the closest corner to the center of the gradient.closest-sidewill meet the side of the box closest to the center of the gradient.farthest-sidewill do the opposite toclosest-side.
You can add as many color stops as you like, just like with the linear-gradient.
Likewise, you can add as many radial-gradients as you like too.
Conic gradient
{% BrowserCompat 'css.types.image.gradient.conic-gradient' %}
A conic gradient has a center point in your box and starts from the top (by default), and goes around in a 360 degree circle.
.my-element {
background: conic-gradient(white, black);
}
The conic-gradient()
function accepts position and angle arguments.
By default, the angle is 0 degrees which starts at the top, in the center.
If you were to set the angle to be 45deg, it would be the top right corner.
The angle argument accepts any type of angle value, like the linear and radial gradients.
The position is center by default. As with radial and linear gradients, positioning can be keyword-based, or it can be defined with numeric values.
You can add as many color stops as you want, like with other gradient types. A good use case for this capability, with conic gradients is rendering pie charts with CSS.
Repeating and mixing
Each type of gradient has a repeating type, too.
These are
repeating-linear-gradient(),
repeating-radial-gradient() and
repeating-conic-gradient().
They are similar to the non-repeating functions and take the same arguments.
The difference is that if the defined gradient can be repeated to fill the box,
based on both of their sizes, it will.
If your gradient doesn't appear to be repeating,
you probably haven't set a length for one of the color stops.
For example,
you can create a striped background with a repeating-linear-gradient by setting color stop lengths.
.my-element {
background: repeating-linear-gradient(
45deg,
red,
red 30px,
white 30px,
white 60px
);
}
You can also mix gradient functions on background properties,
as well as defining as many gradients as you like,
just like you would with a background image.
For example, you can mix multiple linear-gradients together, or two linear-gradients with a radial gradient.
Resources
- Conic.css - a useful collection of conic gradients
- MDN guide to gradients
- Gradient generator
{% Assessment 'gradients' %}
Grid
A really common layout in web design is a header, sidebar, body and footer layout.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/tj7KmP72RKkffGQRswpA.svg", alt="A header with logo and navigation with a sidebar and content area that features an article", width="800", height="531" %}
Over the years, there have been many methods to solve this layout, but with CSS grid, not only is it relatively straightforward, but you have lots of options. Grid is exceptionally useful at combining the control that extrinsic sizing provides with the flexibility of intrinsic sizing, which makes it ideal for this sort of layout. This is because grid is a layout method designed for two-dimensional content. That is, laying things out in rows and columns at the same time.
When creating a grid layout you define a grid with rows and columns. Then you place items onto that grid, or allow the browser to auto-place them into the cells you have created. There's a lot to grid, but with an overview of what is available you'll be making grid layouts in no time.
Overview
So what can you do with grid? Grid layouts have the following features. You'll learn about all of them in this guide.
- A grid can be defined with rows and columns. You can choose how to size these row and column tracks or they can react to the size of the content.
- Direct children of the grid container will be automatically placed onto this grid.
- Or, you can place the items in the precise location that you want.
- Lines and areas on the grid can be named to make placement easier.
- Spare space in the grid container can be distributed between the tracks.
- Grid items can be aligned within their area.
Grid terminology
Grid comes with a bunch of new terminology as it's the first time CSS has had a real layout system.
Grid lines
A grid is made up of lines, which run horizontally and vertically. If your grid has four columns, it will have five column lines including the one after the last column.
Lines are numbered starting from 1, with the numbering following the writing mode and script direction of the component. This means that column line 1 will be on the left in a left-to-right language such as English, and on the right in a right-to-left language such as Arabic.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/Sf8WXYmbhZkbhhPeqTrY.svg", alt="A diagram representation of grid lines", width="800", height="434" %}
Grid tracks
A track is the space between two grid lines. A row track is between two row lines and a column track between two column lines. When we create our grid we create these tracks by assigning a size to them.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/7YkhnpgOQLrcaxjlKmlU.svg", alt="A diagram representation of a grid track", width="800", height="434" %}
Grid cell
A grid cell is the smallest space on a grid defined by the intersection of row and column tracks. It's just like a table cell or a cell in a spreadsheet. If you define a grid and don't place any of the items they will automatically be laid out one item into each defined grid cell.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/lpiPCpW6fy4BjFOL6j77.svg", alt="A diagram representation of a grid cell", width="800", height="434" %}
Grid area
Several grid cells together. Grid areas are created by causing an item to span over multiple tracks.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/pGmMokRfoVbLNxf1VXrF.svg", alt="A diagram representation of a grid area", width="800", height="434" %}
Gaps
A gutter or alley between tracks. For sizing purposes these act like a regular track. You can't place content into a gap but you can span grid items across it.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/JNXSRH4j77loSB099E04.svg", alt="A diagram representation of a grid with gaps", width="800", height="434" %}
Grid container
The HTML element which has display: grid applied,
and therefore creates a new grid formatting context for the direct children.
.container {
display: grid;
}
Grid item
A grid item is an item which is a direct child of the grid container.
<div class="container">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
Rows and columns
To create a basic grid you can define a grid with three column tracks, two row tracks and a 10 pixel gap between the tracks as follows.
.container {
display: grid;
grid-template-columns: 5em 100px 30%;
grid-template-rows: 200px auto;
gap: 10px;
}
This grid demonstrates many of the things described in the terminology section. It has three column tracks. Each track uses a different length unit. It has two row tracks, one using a length unit and the other auto. When used as a track sizing auto can be thought of as being as big as the content. Tracks are auto sized by default.
If the element with a class of .container has child items,
they will immediately lay out on this grid. You can see this in action in the demo below.
The grid overlay in Chrome DevTools can help you understand the various parts of the grid.
Open the demo in Chrome.
Inspect the element with the grey background, which has an ID of container.
Highlight the grid by selecting the grid badge in the DOM, next to the .container element.
Inside the Layout tab,
select Display Line Numbers in the dropdown to see the line numbers on your grid.
Intrinsic sizing keywords
In addition to the length and percentage dimensions as described in the section on sizing units, grid tracks can use intrinsic sizing keywords. These keywords are defined in the Box Sizing specification and add additional methods of sizing boxes in CSS, not just grid tracks.
min-contentmax-contentfit-content()
The min-content
keyword will make a track as small as it can be without the track content overflowing.
Changing the example grid layout to have three column tracks all at min-content size
will mean they become as narrow as the longest word in the track.
The max-content
keyword has the opposite effect.
The track will become as wide enough for all of the content to display in one long unbroken string.
This might cause overflows as the string will not wrap.
The fit-content()
function acts like max-content at first.
However, once the track reaches the size that you pass into the function,
the content starts to wrap.
So fit-content(10em) will create a track that is less than 10em,
if the max-content size is less than 10em,
but never larger than 10em.
In the next demo try out the different intrinsic sizing keywords by changing the sizing of the grid tracks.
{% Aside %} You might spot in this demo that when auto is used the grid columns stretch to fill the container. Auto sized tracks will stretch by default if there is additional space in the grid container. {% endAside %}
The fr unit
We have existing length dimensions, percentages, and also these new keywords.
There is also a special sizing method which only works in grid layout.
This is the fr unit,
a flexible length which describes a share of the available space in the grid container.
The fr unit works in a similar way to using flex: auto in flexbox.
It distributes space after the items have been laid out.
Therefore to have three columns which all get the same share of available space:
.container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
Using different values for the fr unit will share the space in proportion. Larger values getting more of the spare space. In the demo below change the value of the third track.
The minmax() function
{% BrowserCompat 'css.properties.grid-template-columns.minmax' %}
This function means that you can set a minimum and a maximum size for a track.
This can be quite useful.
If we take the example of the fr unit above which distributes remaining space,
it could be written out using
minmax() as minmax(auto, 1fr).
Grid looks at the intrinsic size of the content,
then distributes available space after giving the content enough room.
This means that you might not get tracks that each have an equal share
of all space available in the grid container.
To force a track to take an equal share of the space in the grid container minus gaps use minmax.
Replace 1fr as a track size with minmax(0, 1fr).
This makes the minimum size of the track 0 and not the min-content size.
Grid will then take all of the available size in the container,
deduct the size needed for any gaps,
and share the rest out according to your fr units.
repeat() notation
{% BrowserCompat 'css.properties.grid-template-columns.repeat' %}
If you want to create a 12 column track grid with equal columns, you could use the following CSS.
.container {
display: grid;
grid-template-columns:
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr),
minmax(0,1fr);
}
Or, you could write it out using
repeat():
.container {
display: grid;
grid-template-columns: repeat(12, minmax(0,1fr));
}
The repeat() function can be used to repeat any section of your track listing.
For example you can repeat a pattern of tracks.
You can also have some regular tracks and a repeating section.
.container {
display: grid;
grid-template-columns: 200px repeat(2, 1fr 2fr) 200px; /*creates 6 tracks*/
}
auto-fill and auto-fit
You can combine everything you have learned about track sizing,
minmax(), and repeat,
to create a useful pattern with grid layout.
Perhaps you don't want to specify the number of column tracks,
but instead want to create as many as will fit in your container.
You can achieve this with repeat() and the auto-fill or auto-fit keywords.
In the demo below grid will create as many 200 pixel tracks as will fit in the container.
Open the demo in a new window and see how the grid changes as you change the size of the viewport.
In the demo we get as many tracks as will fit.
The tracks are not flexible however.
You will get a gap on the end until there is enough space for another 200 pixel track.
If you add the minmax() function,
you can request as many tracks as will fit with a minimum size of 200 pixels and a maximum of 1fr.
Grid then lays out the 200 pixel tracks and whatever space is leftover is distributed equally to them.
This creates a two-dimensional responsive layout with no need for any media queries.
There is a subtle difference between auto-fill and auto-fit.
In the next demo play with a grid layout using the syntax explained above,
but with only two grid items in the grid container.
Using the auto-fill keyword you can see that empty tracks have been created.
Change the keyword to auto-fit and the tracks collapse down to 0 size.
This means that the flexible tracks now grow to consume the space.
The auto-fill and auto-fit keywords otherwise act in exactly the same way.
There is no difference between them once the first track is filled.
Auto-placement
You have already seen grid auto-placement at work in the demos so far. Items are placed on the grid one per cell in the order that they appear in the source. For many layouts this might be all you need. If you need more control then there are a couple of things you might like to do. The first is to tweak the auto-placement layout.
Placing items in columns
The default behavior of grid layout is to place items along the rows.
You can instead cause the items to place into columns using grid-auto-flow: column.
You need to define row tracks otherwise the items will create intrinsic column tracks,
and layout out all in one long row.
These values relate to the writing mode of the document.
A row always runs in the direction a sentence runs in the writing mode of the document or component.
In the next demo you can change mode the value of grid-auto-flow and the writing-mode property.
Spanning tracks
You can cause some or all of the items in an auto-placed layout to span more than one track.
Use the span keyword plus the number of lines to span as a value for grid-column-end or grid-row-end.
.item {
grid-column-end: span 2; /* will span two lines, therefore covering two tracks */
}
As you have not specified a grid-column-start,
this uses the initial value of auto and is placed according to the auto-placement rules.
You can also specify the same thing using the shorthand grid-column:
.item {
grid-column: auto / span 2;
}
Filling gaps
An auto-placed layout with some items spanning multiple tracks
may result in a grid with some unfilled cells.
The default behavior of grid layout with a fully auto-placed layout
is to always progress forward.
The items will be placed according to the order they are in the source,
or any modification with the order property.
If there is not enough space to fit an item,
grid will leave a gap and move to the next track.
The next demo shows this behavior.
The checkbox will apply the dense packing mode.
This is enabled by giving grid-auto-flow a value of dense.
With this value in place,
grid will take items later in the layout and use them to fill gaps.
This may mean that the display becomes disconnected from the logical order.
Placing items
You have a lot of functionality from CSS Grid already. Let's now take a look at how we position items on the grid we have created.
The first thing to remember is that CSS Grid Layout is based on a grid of numbered lines. The simplest way to place things onto the grid is to place them from one line to another. You'll discover other ways of placing items in this guide, but you always have access to those numbered lines.
The properties that you can use to place items by line number are:
They have shorthands which allow you to set both start and end lines at once:
To place your item set the start and end lines of the grid area that it should be placed into.
.container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(2, 200px 100px);
}
.item {
grid-column-start: 1; /* start at column line 1 */
grid-column-end: 4; /* end at column line 4 */
grid-row-start: 2; /*start at row line 2 */
grid-row-end: 4; /* end at row line 4 */
}
Chrome DevTools can give you a visual guide to the lines in order to check where your item is placed.
The line numbering follows the writing mode and direction of the component. In the next demo change the writing mode or direction to see how the placement of the items stays consistent to the way that text flows.
Stacking items
Using line-based positioning you can place items into the same cell of the grid.
This means that you can stack items,
or cause one item to partly overlap another.
Items which come later in the source will be displayed on top of items that come earlier.
You can change this stacking order using z-index just as with positioned items.
Negative line numbers
When you create a grid using grid-template-rows and grid-template-columns
you create what is known as the explicit grid.
This is a grid that you have defined and given size to the tracks.
Sometimes you will have items which display outside this explicit grid.
For example,
you might define column tracks and then add several rows of grid items without ever defining row tracks.
The tracks would be auto sized by default.
You also might place an item using grid-column-end that is outside of the explicit grid defined.
In both of these cases grid will create tracks to make the layout work,
and these tracks are referred to as the implicit grid.
Most of the time it will make no difference if you are working with an implicit or explicit grid. However, with line-based placement you may run into the main difference between the two.
Using negative line numbers you can place items from the end line of the explicit grid.
This can be useful if you want an item to span from the first to the last column line.
In that case you can use grid-column: 1 / -1.
The item will stretch right across the explicit grid.
This only works for the explicit grid however. Take a layout of three rows of auto-placed items where you would like the very first item to span to the end line of the grid.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/Dt8yG376MqSyWJJ8KqPr.svg", alt="A sidebar with 8 sibling grid items", width="800", height="359" %}
You might think that you can give that item grid-row: 1 / -1.
In the demo below you can see that this doesn't work.
The tracks are created in the implicit grid,
there is no way to reach the end of the grid using -1.
Sizing implicit tracks
The tracks created in the implicit grid will be auto-sized by default.
However if you want to control the sizing of the rows,
use the
grid-auto-rows property,
and for columns
grid-auto-columns.
To create all implicit rows at a minimum size of 10em and a maximum size of auto:
.container {
display: grid;
grid-auto-rows: minmax(10em, auto);
}
To create implicit columns with a pattern of 100px and 200px wide tracks. In this case the first implicit column will be 100px, the second 200px, the third 100px, and so on.
.container {
display: grid;
grid-auto-columns: 100px 200px;
}
Named grid lines
It can make it easier to place items into a layout if the lines have a name rather than a number. You can name any line on your grid by adding a name of your choosing between square brackets. Multiple names can be added, separated by a space inside the same brackets. Once you have named lines they can be used instead of the numbers.
.container {
display: grid;
grid-template-columns:
[main-start aside-start] 1fr
[aside-end content-start] 2fr
[content-end main-end]; /* a two column layout */
}
.sidebar {
grid-column: aside-start / aside-end;
/* placed between line 1 and 2*/
}
footer {
grid-column: main-start / main-end;
/* right across the layout from line 1 to line 3*/
}
Grid Template Areas
You can also name areas of the grid and place items onto those named areas. This is a lovely technique as it allows you to see what your component looks like right there in the CSS.
To start, give the direct children of your grid container a name using the
grid-area property:
header {
grid-area: header;
}
.sidebar {
grid-area: sidebar;
}
.content {
grid-area: content;
}
footer {
grid-area: footer;
}
The name can be anything you like other than the keywords auto and span.
Once all of your items are named,
use the
grid-template-areas
property to define which grid cells each item will span.
Each row is defined within quotes.
.container {
display: grid;
grid-template-columns: repeat(4,1fr);
grid-template-areas:
"header header header header"
"sidebar content content content"
"sidebar footer footer footer";
}
There are a few rules when using grid-template-areas.
- The value must be a complete grid with no empty cells.
- To span tracks repeat the name.
- The areas created by repeating the name must be rectangular and cannot be disconnected.
If you break any of the above rules the value is treated as invalid and thrown away.
To leave white space on the grid use a . or multiples with no white space between them.
For example to leave the very first cell on the grid empty I could add a series of . characters:
.container {
display: grid;
grid-template-columns: repeat(4,1fr);
grid-template-areas:
"....... header header header"
"sidebar content content content"
"sidebar footer footer footer";
}
As your entire layout is defined in one place,
it makes it straightforward to redefine the layout using media queries.
In the next example I have created a two column layout which moves to three columns
by redefining the value of grid-template-columns and grid-template-areas.
Open the example in a new window to play with the viewport size and see the layout change.
You can also see how the grid-template-areas property relates to writing-mode and direction,
as with other grid methods.
Shorthand properties
There are two shorthand properties which allow you to set many of the grid properties in one go. These can look a little confusing until you break down exactly how they go together. Whether you want to use them or prefer to use longhands is up to you.
grid-template
{% BrowserCompat 'css.properties.grid-template' %}
The grid-template
property is a shorthand for grid-template-rows, grid-template-columns and grid-template-areas.
The rows are defined first,
along with the value of grid-template-areas.
Column sizing is added after a /.
.container {
display: grid;
grid-template:
"head head head" minmax(150px, auto)
"sidebar content content" auto
"sidebar footer footer" auto / 1fr 1fr 1fr;
}
grid property
{% BrowserCompat 'css.properties.grid' %}
The grid
shorthand can be used in exactly the same way as the grid-template shorthand.
When used in this way it will reset the other grid properties that it accepts to their initial values.
The full set being:
grid-template-rowsgrid-template-columnsgrid-template-areasgrid-auto-rowsgrid-auto-columnsgrid-auto-flow
You can alternately use this shorthand to define how the implicit grid behaves, for example:
.container {
display: grid;
grid: repeat(2, 80px) / auto-flow 120px;
}
Alignment
Grid layout uses the same alignment properties that you learned about in the guide to
flexbox.
In grid the properties which begin with justify- are always used on the inline axis,
the direction in which sentences run in your writing mode.
The properties which begin with align- are used on the block axis,
the direction in which blocks are laid out in your writing mode.
justify-contentandalign-content: distribute additional space in the grid container around or between tracks.justify-selfandalign-self: are applied to a grid item to move it around inside the grid area it is placed in.justify-itemsandalign-items: are applied to the grid container to set all of thejustify-selfproperties on the items.
Distributing extra space
In this demo the grid is larger than the space needed to lay out the fixed width tracks.
This means we have space in both the inline and block dimensions of the grid.
Try different values of align-content and justify-content to see how the tracks behave.
Note how the gaps become larger when using values such as space-between,
and any grid item spanning two tracks also grows to absorb the additional space added to the gap.
{% Aside %} As with flexbox, these properties will only work if there is additional space to distribute. If your grid tracks neatly fill the container then there will be no space to share out. {% endAside %}
Moving content around
Items with a background color appear to completely fill the grid area they are placed in,
because the initial value for justify-self and align-self is stretch.
{% Aside %}
If your item is an image,
or something else with an intrinsic aspect ratio,
the initial value will be start rather than stretch to avoid stretching things out of shape.
{% endAside %}
In the demo change the values of justify-items and align-items to see how this changes the layout.
The grid area is not changing size,
instead the items are being moved around inside the defined area.
{% Assessment 'grid' %}
Resources
This guide has given you an overview of the different parts of the grid layout specification. To find out more, take a look at the following resources.
- MDN CSS Grid Layout
- A Complete Guide to Grid
- Creating a Grid Container
- Inspect CSS grid in Chrome DevTools
- A comprehensive collection of grid learning material
Inheritance
Say you just wrote some CSS to make elements look like a button.
<a href="http://example.com" class="my-button">I am a button link</a>
.my-button {
display: inline-block;
padding: 1rem 2rem;
text-decoration: none;
background: pink;
font: inherit;
text-align: center;
}
You then add a link element to an article of content,
with a class value of .my-button. However there's an issue,
the text is not the color that you expected it to be. How did this happen?
Some CSS properties inherit if you don't specify a value for them.
In the case of this button, it inherited the color from this CSS:
article a {
color: maroon;
}
In this lesson you'll learn why that happens and how inheritance is a powerful feature to help you write less CSS.
Inheritance flow
Let's take a look at how inheritance works, using this snippet of HTML:
<html>
<body>
<article>
<p>Lorem ipsum dolor sit amet.</p>
</article>
</body>
</html>
The root element (<html>) won't inherit anything because it is the first element in the document.
Add some CSS on the HTML element,
and it starts to cascade down the document.
html {
color: lightslategray;
}
The color property is inheritable by other elements.
The html element has color: lightslategray,
therefore all elements that can inherit color will now have a color of lightslategray.
body {
font-size: 1.2em;
}
{% Aside %}
Because this demo sets the font size on the body element,
the html element will still have the initial font size set by the browser (user agent stylesheet),
but the article and p will inherit the font size declared by the body.
This is because inheritance only cascades downwards.
{% endAside %}
p {
font-style: italic;
}
Only the <p> will have italic text because it's the deepest nested element.
Inheritance only flows downwards, not back up to parent elements.
Which properties are inheritable?
Not all CSS properties are inheritable, but there are a lot that are. For reference, here is the entire list of inheritable properties, taken from the W3 reference of all CSS properties:
- azimuth
- border-collapse
- border-spacing
- caption-side
- color
- cursor
- direction
- empty-cells
- font-family
- font-size
- font-style
- font-variant
- font-weight
- font
- letter-spacing
- line-height
- list-style-image
- list-style-position
- list-style-type
- list-style
- orphans
- quotes
- text-align
- text-indent
- text-transform
- visibility
- white-space
- widows
- word-spacing
How inheritance works
Every HTML element has every CSS property defined by default with an initial value. An initial value is a property that's not inherited and shows up as a default if the cascade fails to calculate a value for that element.
Properties that can be inherited cascade downwards,
and child elements will get a computed value which represents its parent's value.
This means that if a parent has font-weight set to bold all child elements will be bold,
unless their font-weight is set to a different value,
or the user agent stylesheet has a value for font-weight for that element.
How to explicitly inherit and control inheritance
Inheritance can affect elements in unexpected ways so CSS has tools to help with that.
The inherit keyword
You can make any property inherit its parent's computed value with the inherit keyword.
A useful way to use this keyword is to create exceptions.
strong {
font-weight: 900;
}
This CSS snippet sets all <strong> elements to have a font-weight of 900,
instead of the default bold value, which would be the equivalent of font-weight: 700.
.my-component {
font-weight: 500;
}
The .my-component class sets font-weight to 500 instead.
To make the <strong> elements inside .my-component also font-weight: 500 add:
.my-component strong {
font-weight: inherit;
}
Now, the <strong> elements inside .my-component will have a font-weight of 500.
You could explicitly set this value,
but if you use inherit and the CSS of .my-component changes in the future,
you can guarantee that your <strong> will automatically stay up to date with it.
The initial keyword
Inheritance can cause problems with your elements and initial provides you with a powerful reset option.
You learned earlier that every property has a default value in CSS.
The initial keyword sets a property back to that initial, default value.
aside strong {
font-weight: initial;
}
This snippet will remove the bold weight from all <strong> elements inside an <aside> element and instead,
make them normal weight, which is the initial value.
The unset keyword
The unset property behaves differently if a property is inheritable or not.
If a property is inheritable,
the unset keyword will be the same as inherit.
If the property is not inheritable, the unset keyword is equal to initial.
Remembering which CSS properties are inheritable can be hard,
unset can be helpful in that context.
For example, color is inheritable,
but margin isn't, so you can write this:
/* Global color styles for paragraph in authored CSS */
p {
margin-top: 2em;
color: goldenrod;
}
/* The p needs to be reset in asides, so you can use unset */
aside p {
margin: unset;
color: unset;
}
Now, the margin is removed and color reverts back to being the inherited computed value.
You can use the unset value with the all property, too.
Going back to the above example,
what happens if the global p styles get an additional few properties?
Only the rule that was set for margin and color will apply.
/* Global color styles for paragraph in authored CSS */
p {
margin-top: 2em;
color: goldenrod;
padding: 2em;
border: 1px solid;
}
/* Not all properties are accounted for anymore */
aside p {
margin: unset;
color: unset;
}
If you change the aside p rule to all: unset instead,
it doesn't matter what global styles are applied to p in the future,
they will always be unset.
aside p {
margin: unset;
color: unset;
all: unset;
}
{% Assessment 'conclusion' %}
Resources
Layout
Imagine you're working as a developer, and a designer colleague hands you a design for a brand new website. The design has all sorts of interesting layouts and compositions: two-dimensional layouts that are considerate of viewport width and height, as well as layouts that need to be fluid and flexible. How do you decide the best way to style these with CSS?
CSS provides us with various ways to solve layout problems, on a horizontal axis, vertical axis, or even both. Choosing the right layout method for a context can be hard, and often you may need more than one layout method to solve your problem. To help with this, in the following modules, you'll learn about the unique features of each CSS layout mechanism to inform those decisions.
Layout: a brief history
In the early days of the web,
designs more complex than a simple document were laid out with <table> elements.
Separating HTML from visual styles was made easier when CSS was widely adopted by browsers in the late '90s.
CSS opened the door to developers being able to completely change the look and feel of a website without ever touching HTML.
This new capability inspired projects such as The CSS Zen Garden,
which was created to demonstrate the power of CSS to encourage more developers to learn it.
CSS has evolved as our needs for web design and browser technology have evolved. You can read how CSS layout and our approach to layout has evolved over time in this article by Rachel Andrew.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/vDDoFFoPVgJEuEaqcP4H.svg", alt="A timeline showing how CSS has evolved over the years, starting in 1996 up to 2021", width="760", height="270" %}
Layout: the present and future
Modern CSS has exceptionally powerful layout tooling. We have dedicated systems for layout and we're going to have a high-level look at what we have at our disposal, before digging into more detail of Flexbox and Grid in the next modules.
Understanding the display property
The display property does two things.
The first thing it does is determine if the box it is applied to acts as inline or block.
.my-element {
display: inline;
}
Inline elements behave like words in a sentence.
They sit next to each other in the inline direction.
Elements such as <span> and <strong>,
which are typically used to style pieces of text within containing elements like a <p> (paragraph),
are inline by default.
They also preserve surrounding whitespace.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/GezxDZXkJgkMevkKg39M.png", alt="A diagram showing all the different sizes of a box and where each sizing section starts and ends", width="800", height="559" %}
You can't set an explicit width and height on inline elements. Any block level margin and padding will be ignored by the surrounding elements.
.my-element {
display: block;
}
Block elements don't sit alongside each other. They create a new line for themselves. Unless changed by other CSS code, a block element will expand to the size of the inline dimension, therefore spanning the full width in a horizontal writing mode. The margin on all sides of a block element will be respected.
.my-element {
display: flex;
}
The display property also determines how an element's children should behave.
For example,
setting the display property to display: flex makes the box a block-level box,
and also converts its children to flex items.
This enables the flex properties that control alignment, ordering and flow.
Flexbox and Grid
There are two main layout mechanisms that create layout rules for multiple elements, flexbox and grid. They share similarities, but are designed to solve different layout problems.
{% Aside %} We will be going into much more detail for both of these in future modules, but here is a high-level overview of what both are and what they are useful for. {% endAside %}
Flexbox
.my-element {
display: flex;
}
Flexbox is a layout mechanism for one-dimensional layouts. Layout across a single axis, either horizontally or vertically. By default, flexbox will align the element's children next to each other, in the inline direction, and stretch them in the block direction, so they're all the same height.
Items will stay on the same axis and not wrap when they run out of space.
Instead they will try to squash onto the same line as each other.
This behaviour can be changed using the align-items, justify-content and flex-wrap properties.
Flexbox also converts the child elements to be flex items,
which means you can write rules on how they behave inside a flex container.
You can change alignment, order and justification on an individual item.
You can also change how it shrinks or grows using the flex property.
.my-element div {
flex: 1 0 auto;
}
The flex property is a shorthand for flex-grow, flex-shrink and flex-basis.
You can expand the above example like this:
.my-element div {
flex-grow: 1;
flex-shrink: 0;
flex-basis: auto;
}
Developers provide these low-level rules to hint a browser how the layout should behave when it is challenged by content and viewport dimensions. This makes it a very useful mechanism for responsive web design.
Grid
.my-element {
display: grid;
}
Grid is similar in a lot of ways to flexbox, but it is designed to control multi-axis layouts instead of single-axis layouts (vertical or horizontal space).
Grid enables you to write layout rules on an element that has display: grid,
and introduces a few new primitives for layout styling,
such as the repeat() and minmax() functions.
One useful grid unit is the fr unit—which is a fraction of remaining space—you can build traditional 12 column grids,
with a gap between each item, with 3 CSS properties:
.my-element {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
}
This example above shows a single axis layout. Where flexbox mostly treats items as a group, grid gives you precise control over their placement in two dimensions. We could define that the first item in this grid takes up 2 rows and 3 columns:
.my-element :first-child {
grid-row: 1/3;
grid-column: 1/4;
}
The grid-row and grid-column properties instruct the first element in the grid to span to the start of the fourth column,
from the first column, then span to the third row, from the first row.
Flow layout
If not using grid or flexbox, your elements display in normal flow. There are a number of layout methods that you can use to adjust the behavior and position of items when in normal flow.
Inline block
Remember how surrounding elements don't respect block margin and padding on an inline element?
With inline-block you can cause that to happen.
p span {
display: inline-block;
}
Using inline-block gives you a box that has some of the characteristics of a block-level element,
but still flows inline with the text.
p span {
margin-top: 0.5rem;
}
Floats
If you have an image that sits within a paragraph of text, wouldn't it be handy for that text to wrap around that image like you might see in a newspaper? You can do this with floats.
img {
float: left;
margin-right: 1em;
}
The float property instructs an element to "float" to the direction specified.
The image in this example is instructed to float left,
which then allows sibling elements to "wrap" around it.
You can instruct an element to float left, right or inherit.
{% Aside 'warning' %}
When you use float, keep in mind that any elements following the floated element may have their layout adjusted.
To prevent this, you can clear the float,
either by using clear: both on an element that follows your floated element
or with display: flow-root on the parent of your floated elements.
Find out more in the article The end of the clearfix hack. {% endAside %}
Multicolumn layout
If you have a really long list of elements, such as a list of all of the countries of the world, it can result in a lot of scrolling and time wasted for a user. It can also create excess whitespace on the page. With CSS multicolumn, you can split this into multiple columns to help with both of these issues.
<h1>All countries</h1>
<ul class="countries">
<li>Argentina</li>
<li>Aland Islands</li>
<li>Albania</li>
<li>Algeria</li>
<li>American Samoa</li>
<li>Andorra</li>
…
</ul>
.countries {
column-count: 2;
column-gap: 1em;
}
This automatically splits that long list into two columns and adds a gap between the two columns.
.countries {
width: 100%;
column-width: 260px;
column-gap: 1em;
}
Instead of setting the number of columns that the content is split into,
you can also define a minimum desired width, using column-width.
As more space is made available in the viewport,
more columns will automatically be created and as space is reduced,
columns will also reduce.
This is very useful in responsive web design contexts.
Positioning
Last on this overview of layout mechanisms is positioning.
The position property changes how an element behaves in the normal flow of the document,
and how it relates to other elements.
The available options are relative, absolute, fixed and sticky with the default value being static.
.my-element {
position: relative;
top: 10px;
}
This element is nudged 10px down based on its current position in the document,
as it is positioned relative to itself.
Adding position: relative to an element also makes it the containing block of any child elements with position: absolute.
This means that its child will now be repositioned to this particular element,
instead of the topmost relative parent, when it has an absolute position applied to it.
.my-element {
position: relative;
width: 100px;
height: 100px;
}
.another-element {
position: absolute;
bottom: 0;
right: 0;
width: 50px;
height: 50px;
}
When you set position to absolute,
it breaks the element out of the current document flow.
This means two things:
- You can position this element wherever you like, using
top,right,bottomandleftin its nearest relative parent. - All of the content surrounding an absolute element reflows to fill the remaining space left by that element.
An element with a position value of fixed behaves in a similar way to absolute,
with its parent being the root <html> element.
Fixed position elements stay anchored from the top left based on the top, right, bottom and left values that you set.
You can achieve the anchored,
fixed aspects of fixed and the more predictable document flow-honoring aspects of relative by using sticky.
With this value, as the viewport scrolls past the element,
it stays anchored to the top, right, bottom and left values that you set.
Wrap-up
There's a lot of choice and flexibility with CSS layout. To dive further into the power of CSS Flexbox and Grid, continue into the next few modules.
{% Assessment 'layout' %}
Lists
Imagine you have a bunch of items you plan to buy during your next grocery trip. One common way to represent this visually is a list—but how can you add styling to your grocery list?
<ul>
<li>oat milk</li>
<li>rhubarb</li>
<li>cereal</li>
<li>pie crust</li>
</ul>
Creating a List
The preceding list started with a semantic element, or <ul>, with grocery list items (<li> elements) as children. If you inspect each <li> element you can see that they all have display: list-item, which is why the browser renders a ::marker by default.
li {
display: list-item;
}
There are two other types of lists.
Ordered lists can be created with <ol>, in which case the list-item will display a number as the ::marker.
<ol>
<li>oat milk</li>
<li>rhubarb</li>
<li>cereal</li>
<li>pie crust</li>
</ol>
And description lists are created with <dl>, however this list type does not use the <li> list item element.
<dl>
<dt>oat milk</dt>
<dd>- non dairy trendy drink</dd>
<dt>cereal</dt>
<dd>- breakfast food</dd>
</dl>
List styles
{% BrowserCompat 'css.properties.list-style-position' %}
Now that you know how to make a list, you can style them. The first CSS properties to discover are those that are applied to the entire list.
There are three list-style properties you can use to style your example: list-style-position, list-style-image, and list-style-type.
list-style-position
list-style-position allows you to move your bullet point to either inside or outside the list-item's contents. The default outside means the bullet point is not included in the list items contents while inside moves the first element among the list item's contents.
{% Img src="image/dQwiPAoChjTYzXYCNg6U6Tsnr3x1/qVlpVCrwKwbGjEJdmqh0.jpg", alt="A list with both outside and inside ::marker which shows that outside (default value) is not in the list-item and is inside the list-item content box.", width="800", height="270" %}
list-style-image
list-style-image allows you to replace your list's bullet points with images. This enables you to set an image such as an url or none to make your bullets an image, svg or gif even. You can also use any media type or even a data URI.
Let's look at how we can add an image of each of our grocery items as the list-style-image:
{% Aside %}
This property is a bit limited in controlling the position, size, etc. of the bullets, so we recommend using the ::marker property for a more customizable approach.
{% endAside %}
list-style-type
The final option is to style the list-style-type which changes the bullet points to known style keywords, custom strings, emojis and more. You can view all of the possible list style types here.
list-style shorthand
Now that we have all of these individual properties, we can use the list-style shorthand to set all of our list styles in one line:
list-style: <'list-style-type'> || <'list-style-position'> || <'list-style-image'>
list-style allows you to declare combinations of one, two, or three of the list-style properties in any order. If list-style-type and list-style-image are both set, then list-style-type is used as a fallback if the image is unavailable.
/* type */
list-style: square;
/* image */
list-style: url('../img/shape.png');
/* position */
list-style: inside;
/* type | position */
list-style: georgian inside;
/* type | image | position */
list-style: lower-roman url('../img/shape.png') outside;
/* Keyword value */
list-style: none;
/* Global values */
list-style: inherit;
list-style: initial;
list-style: revert;
list-style: unset;
This is the most commonly used property of the list styles covered in this section. One common application is list-style: none to hide default styles. Default styles come from the browser, and you often see reset stylesheets removing list styles like padding and margins. You can also use this shorthand to set styles, like list-style: square inside;
So far, the examples have focused on styling an entire list and list items, but what about a more granular approach?
::marker pseudo-element
The list-item marker element is the bullet, hyphen, or roman numeral that helps indicate each item in your list.
{% Img src="image/dQwiPAoChjTYzXYCNg6U6Tsnr3x1/pOWTl4BYyJI7qgbMVeYu.jpg", alt="A list with three items which shows that each of the bullets are ::marker pseudo-elements.", width="800", height="395" %}
If you inspect the list in DevTools, you can see a ::marker element for each of the list items, despite not declaring any in HTML. If you inspect the ::marker further, you'll see the browser default styling for it.
::marker {
unicode-bidi: isolate;
font-variant-numeric: tabular-nums;
text-transform: none;
text-indent: 0px !important;
text-align: start !important;
text-align-last: start !important;
}
When you declare a list, each item is given a marker, despite there being no bullet point or roman numeral in your HTML. This is a pseudo-element because the browser generates it for you, and provides a limited styling API to target it. Learn more about the anatomy of the CSS bullet. ::marker currently has limited support in Safari.
Marker box
In the CSS layout model, list item markers are represented by a marker box associated with each list item. The marker box is the container which typically contains the bullet or number.
To style the marker box, you can use the ::marker selector. This allows you to select just the marker instead of styling based on the entire list.
{% Aside %}
Note: ::marker elements precede any pseudo-elements that you may have inserted using CSS ::before.
{% endAside %}
Marker styles
Now that you have selected the marker, let's look at the styling properties available to this selector. You can learn more about Custom bullets with CSS ::marker on web.dev.
There are quite a few allowed CSS ::marker Properties:
animation-*transition-*colordirectionfont-*contentunicode-bidiwhite-space
{% Aside %}
In ordered lists, the bullets default to numbers. The ::marker content value is a use case for counters to create custom numbering.
{% endAside %}
Display type
All of our list-style and ::marker properties know to style <li> elements because they have a default display value of list-item. You can also make things that aren't an <li> into a list item.
You do this by adding the property display: list-item. One example of using display: list-item is if you want a hanging bullet on a heading, so that you can change it to something else with ::marker. The following example shows a heading using display: list-item for styling purposes, with a list using correct list markup below.
While you can turn anything into a list-item view with display, you should not use this instead of using correct list markup, if the content you are styling really is a list. Changing the visual appearance of an item to a list item does not change how accessibility services read and recognize the item, so it will not be read as a list item to screen readers or switch devices. You should always use semantic markup and create lists with <li> whenever possible.
{% Assessment 'lists' %}
Resources
- MDN Guide on Styling Lists
- Custom bullets with CSS ::marker
- Smashing Magazine: CSS Lists, Markers and Counters
- MDN Using CSS Counters
- CSS Lists and Counters Module Level 3
- CSS-Tricks: Counting With CSS Counters and CSS Grid
Logical Properties
A really common user interface pattern is a text label with a supporting inline icon.
The icon sits to the left of the text with a small gap between the two,
provided by margin-right on the icon.
There's a problem though,
because this will only work when the text direction is left to right.
If the text direction changed to right-to-left—which is how languages like Arabic read—the icon will sit up against the text.
How do you account for this in CSS? Logical properties allow you to resolve these situations. Among many other benefits, they provide free, automatic support for internationalization. They help you build a more resilient, inclusive front-end.
Terminology
The physical properties of top, right, bottom, and left refer to the physical dimensions of the viewport. You can think of these like a compass rose on a map. Logical properties, on the other hand, refer to the edges of a box as they relate to the flow of content. Therefore, they can change if the text direction or writing mode changes. This is a big shift from directional styles, and it gives us a lot more flexibility when styling our interfaces.
Block flow
Block flow is the direction in which content blocks are placed. For example, if there are two paragraphs, the block flow is where the second paragraph will go. In an English document, the block flow is top-to-bottom. Think of this in the context of paragraphs of text following each other, top-to-bottom.
Inline flow
The inline flow is how text flows in a sentence.
In an English document the inline flow is left to right.
If you were to change the document language of your webpage to Arabic (<html lang="ar">),
then the inline flow would be right-to-left.
Text flows in the direction determined by the document's writing mode.
You can change the direction that text is laid out with the writing-mode property.
This can be applied to the entire document, or to individual elements.
Flow relative
Historically in CSS,
we have only been able to apply properties like margin relative to the direction of their sides.
For example, margin-top is applied to the physical top of the element.
With logical properties, margin-top becomes margin-block-start.
This means that regardless of language and text direction,
the block flow has appropriate margin rules.
Sizing
To prevent an element exceeding a certain width or height, write a rule like this:
.my-element {
max-width: 150px;
max-height: 100px;
}
The flow-relative equivalents are max-inline-size and max-block-size.
You can also use min-block-size and min-inline-size instead of min-height and min-width.
With logical properties, that max width and height rule would look like this:
.my-element {
max-inline-size: 150px;
max-block-size: 100px;
}
Start and end
Instead of using directions such as top, right, bottom and left, use start and end. This gives you block-start, inline-end, block-end, and inline-start. These allow you to apply CSS properties that respond to writing mode changes.
For example, to align text to the right, you could use this CSS:
p {
text-align: right;
}
If your aim is not to align to the physical right,
but rather to the start of the reading direction,
this isn't helpful.
With logical values, there are more helpful start and end values which map to the text direction.
The text alignment rule now looks like this:
p {
text-align: end;
}
Spacing and positioning
Logical properties for margin, padding and inset
make positioning elements, and determining how these elements interact with each other across writing modes easier and more efficient.
The margin and padding related properties are still direct mappings to directions,
but the key difference is that when a writing mode changes, they change with it.
.my-element {
padding-top: 2em;
padding-bottom: 2em;
margin-left: 2em;
position: relative;
top: 0.2em;
}
This adds some vertical inside space with padding and pushes it out from the left with margin.
The top property also shifts it downwards.
With logical property equivalents, it would instead look like this:
.my-element {
padding-block-start: 2em;
padding-block-end: 2em;
margin-inline-start: 2em;
position: relative;
inset-block-start: 0.2em;
}
This adds some inline inside space with padding and pushes it out from the inline-start with margin.
The inset-block property moves it inwards from the block-start.
The inset-block property isn't the only shorthand option with logical properties.
This rule can be further condensed,
using shorthand versions of the margin and padding properties.
.my-element {
padding-block: 2em;
margin-inline: 2em 0;
position: relative;
inset-block: 0.2em 0;
}
Borders
Adding border and border-radius can also be done with logical properties.
To add a border on the bottom and right, with a right-hand radius, you might write a rule like this:
.my-element {
border-bottom: 1px solid red;
border-right: 1px solid red;
border-bottom-right-radius: 1em;
}
Or, you could use logical properties like this:
.my-element {
border-block-end: 1px solid red;
border-inline-end: 1px solid red;
border-end-end-radius: 1em;
}
The end-end in border-end-end-radius is the block end and inline end.
Units
Logical properties bring two new units: vi and vb.
A vi unit is 1% of the viewport size in the inline direction.
The non-logical property equivalent is vw.
The vb unit is 1% of the viewport in the block direction.
The non-logical property equivalent is vh.
These units will always map to the reading direction.
For example, if you want an element to take up 80% of the available inline space of a viewport,
using the vi unit will automatically switch that size to be top to bottom if the writing mode is vertical.
Using logical properties pragmatically
Logical properties and writing modes are not just for internationalization. You can use them to produce a more versatile user interface.
If you have a chart that has labels on the X axis and Y axis, you might want the text on the Y label to read vertically.
Because the Y axis label in the demo has a writing-mode of vertical-rl,
you can use the same margin values on both labels.
The margin-block-start value applies to both labels
because the block start is on the right for the Y axis and on the top for the X axis.
The block-start sides have a red border so you can see them.
Solving the icon issue
Now that we've covered logical properties, this knowledge can be applied to the design problem we started with.
p {
display: inline-flex;
align-items: center;
}
p svg {
width: 1.2em;
height: 1.2em;
margin-right: 0.5em;
flex: none;
}
The margin has been applied to the right of the icon element.
In order for the spacing between the icon and the text to support all reading direction,
the margin-inline-end property needs to be used instead.
p {
display: inline-flex;
align-items: center;
}
p svg {
width: 1.2em;
height: 1.2em;
margin-inline-end: 0.5em;
flex: none;
}
Now, regardless of reading direction, the icon will position itself and space itself appropriately.
{% Assessment 'logical-props' %}
Overflow
When content extends beyond its parent, there are many options for how you can handle it. You can scroll to add additional space, clip the overflowing edges, indicate the cut-off with an ellipsis, and so much more. Overflow is especially important to consider when developing for phone applications and multiple screen sizes.
There are two different clipping options in CSS; text-overflow will help with individual lines of text, and the overflow properties will help control overflow in the box model.
Single line overflow with text-overflow
Use the text-overflow property on any element that contains text node(s), for example a paragraph, <p>. It specifies how the text appears when it doesn’t fit in the available space of the element. All viewable HTML text on a page is in text nodes. To use text-overflow you need a single unwrapped line of text, so you must also set overflow to hidden and have a white-space value that prevents wrapping.
p {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
Using overflow properties
Overflow properties are set on an element to control what happens when its children need more space than it has available. This can be intentional, like in an interactive map like Google Maps, where a user pans around a large image clipped to a specific size. It can also be unintentional like in a chat application where the user types a lengthy message that doesn’t fit in the text bubble.
You can think of the overflow in two parts. The parent element has a firmly constrained space that will not change. You can think of this as a window. The child elements are content that want more space from the parent. You can think of this as what you are looking through the window at. Managing overflow will help guide how the window frames this content.
Scrolling on the vertical and horizontal axis
The overflow-y property controls physical overflow along the vertical axis of the device viewport, therefore scrolling up and down.
The overflow-x property controls overflow along the horizontal axis of the device viewport, therefore scrolling left and right.
Logical properties for scroll direction
{% BrowserCompat 'css.properties.overflow-inline' %}
The overflow-inline and overflow-block properties set the overflow based on the document direction and writing mode.
The overflow shorthand
The overflow shorthand sets both overflow-x and overflow-y styles in one line:
If two keywords are specified, the first applies to overflow-x and the second to overflow-y. Otherwise, both overflow-x and overflow-y use the same value.
Values
Let's take a closer look at the values and keywords available for the overflow properties.
overflow: visible (default)
: Without setting the property, overflow: visible is the default value for the web. This ensures that content is never unintentionally hidden and follows the core tenets of "never hide content" or "safe layouts of precise layouts".
overflow: scroll
: overflow: scroll enables scrollbars to allow users to scroll through content. Even if content isn't currently overflowing, scrollbars will be present. This is a great way to reduce future layout shift if a container may be scrollable in the future based, for example, on resize, and visually prepare the user for scrollable areas.
overflow: clip
: Like overflow: hidden, the content with overflow: clip is clipped to the element's padding box. The difference between clip and hidden is that the clip keyword also forbids all scrolling, including programmatic scrolling.
overflow: auto
: Finally, the value most commonly used, overflow: auto. This respects the user's preferences and shows scrollbars if needed, but hides them by default, and gives responsibility for scrolling to the user and browser.
{% Aside %}
Using the overflow property with a value other than visible creates a block formatting context.
{% endAside %}
Scrolling and overflow
Many of these overflow behaviors introduce a scrollbar, but there’s a few specific scroll behaviors and properties that can help you control scrolling on your overflow container.
Scrolling and accessibility
It's important to make sure that the scrollable area can accept focus so that a keyboard user can tab to the box, then use the arrow keys to scroll.
To allow a scrolling box to accept focus add tabindex="0", a name with the aria-labelledby attribute, and an appropriate role attribute. For example:
<div tabindex="0" role="region" aria-labelledby="id-of-descriptive-text">
content
</div>
CSS can then be used to indicate that the box has focus, using the outline property to give a visual clue that it will now be scrollable.
In Using CSS to Enforce Accessibility Adrian Roselli demonstrates how CSS can help prevent accessibility regressions. For example, to only turn on scrolling and add the focus indicator if the correct attributes are used. The following rules will only make the box scrollable if it has a tabindex, aria-labelledby, and role attribute.
[role][aria-labelledby][tabindex] {
overflow: auto;
}
[role][aria-labelledby][tabindex]:focus {
outline: .1em solid blue;
}
Scrollbar positioning within the box model
Scroll bars take up space within the padding box and can compete for space if inline and not overlayed. The box model module expands more on this potential source of layout shift.
root-scroller vs implicit-scroller
You may notice that some scrollers have a pull-to-refresh behavior and other special behaviors, especially when developing for mobile and hybrid applications. This scroll behavior happens on the root scroller. There is only ever one root scroller on a page. By default, the documentElement is the page's root scroller, however, by changing which element is the root scroller, the special behaviors can be applied to scrollers other than the documentElement, we call this new scroller the implicit root scroller.
To create a root scroller, you can use something called scroller promotion by positioning a container as fixed, ensuring it covers the entire viewport and is z-index on top with a scroller. Experience a root scroller vs a nested implicit scroller here.
compared to scrolling an implicit scroller with no enhanced scroll behavior.
scroll-behavior
{% BrowserCompat 'css.properties.scroll-behavior' %}
scroll-behavior allows you to opt into browser-controlled scrolling to elements. This allows you to specify how in-page navigation like .scrollTo() or links are handled.
This is especially useful when used with prefers-reduced-motion to specify scroll behavior based on user preference.
@media (prefers-reduced-motion: no-preference) {
.scroll-view {
scroll-behavior: auto;
}
}
overscroll-behavior
{% BrowserCompat 'css.properties.overscroll-behavior' %}
If you’ve ever reached the end of a modal overlay, then continued scrolling and had the page behind the overlay move, this is the scroll chaining, or bubbling up to the parent scroll container. The overscroll-behavior property allows you to prevent overflow scrolling leaking into a parent container (called scroll chaining).
{% Assessment 'overflow' %}
Resources
Overflow And Data Loss In CSS from Smashing Magazine
Pseudo-classes
Say you've got an email sign up form,
and you want the email form field to have a red border if it contains an invalid email address.
How do you do that?
You can use an :invalid CSS pseudo-class,
which is one of many browser-provided pseudo-classes.
A pseudo-class lets you apply styles based on state changes and external factors. This means that your design can react to user input such as an invalid email address. These are covered in the selectors module, and this module will take you through them in more detail.
Unlike pseudo-elements, which you can learn more about in the previous module, pseudo-classes hook onto specific states that an element might be in, rather than generally style parts of that element.
Interactive states
The following pseudo-classes apply due to an interaction a user has with your page.
:hover
{% BrowserCompat 'css.selectors.hover' %}
If a user has a pointing device like a mouse or trackpad,
and they place it over an element,
you can hook on to that state with
:hover to apply styles.
This is a useful way to hint that an element can be interacted with.
:active
{% BrowserCompat 'css.selectors.active' %}
This state is triggered when an element is actively being interacted with— such as a click—before click is released. If a pointing device like a mouse is used, this state is when the click starts and hasn't yet been released.
:focus, :focus-within, and :focus-visible
{% BrowserCompat 'css.selectors.focus' %}
If an element can receive focus—like a <button>—
you can react to that state with the
:focus pseudo-class.
You can also react if a child element of your element receives focus with
:focus-within.
Focusable elements, like buttons, will show a focus ring when they are in focus—even when clicked. In this sort of situation, a developer will apply the following CSS:
button:focus {
outline: none;
}
This CSS removes the default browser focus ring when an element receives focus,
which presents an accessibility issue for users who navigate a web page with a keyboard.
If there is no focus style,
they won't be able to keep track of where focus currently is when using the tab key.
With :focus-visible
you can present a focus style when an element receives focus via the keyboard,
while also using the outline: none rule to prevent it when a pointer device interacts with it.
button:focus {
outline: none;
}
button:focus-visible {
outline: 1px solid black;
}
:target
{% BrowserCompat 'css.selectors.target' %}
The :target
pseudo-class selects an element that has an id matching a URL fragment.
Say you have the following HTML:
<article id="content">
…
</article>
You can attach styles to that element when the url contains #content.
#content:target {
background: yellow;
}
This is useful for highlighting areas that might have been specifically linked to, such as the main content on a website, via a skip link.
Historic states
:link
{% BrowserCompat 'css.selectors.link' %}
The :link
pseudo-class can be applied to any <a> element that has a href value that hasn't been visited yet.
:visited
You can style a link that's already been visited by the user using the
:visited pseudo-class.
This is the opposite state to :link but you have fewer CSS properties to use for
security reasons.
You can only style color, background-color,
border-color, outline-color and the color of SVG fill and stroke.
Order matters
If you define a :visited style,
it can be overridden by a link pseudo-class with at least equal specificity.
Because of this,
it's recommended that you use the LVHA rule for styling links with pseudo-classes in a particular order:
:link, :visited, :hover, :active.
a:link {}
a:visited {}
a:hover {}
a:active {}
{% Aside %}
For security reasons,
you can only change styles defined by a :link or unvisited state with the :visited pseudo-class,
so making sure you define changeable styles first is important.
Sticking to the LVHA rule will help with that.
{% endAside %}
Form states
The following pseudo-classes can select form elements, in the various states that these elements might be in during interaction with them.
:disabled and :enabled
{% BrowserCompat 'css.selectors.disabled' %}
If a form element,
such as a <button> is disabled by the browser,
you can hook on to that state with the
:disabled pseudo-class.
The :enabled
pseudo-class is available for the opposite state,
though form elements are also :enabled by default,
therefore you might not find yourself reaching for this pseudo-class.
:checked and :indeterminate
{% BrowserCompat 'css.selectors.checked' %}
The :checked
pseudo-class is available when a supporting form element,
such as a checkbox or radio button is in a checked state.
The :checked state is a binary(true or false) state,
but checkboxes do have an in-between state when they are neither checked or unchecked.
This is known as the
:indeterminate state.
An example of this state is when you have a "select all" control that checks all checkboxes in a group. If the user was to then uncheck one of these checkboxes, the root checkbox would no longer represent "all" being checked, so should be put into an indeterminate state.
The <progress> element also has an indeterminate state that can be styled.
A common use case is to give it a striped appearance to indicate it's unknown how much more is needed.
:placeholder-shown
{% BrowserCompat 'css.selectors.placeholder-shown' %}
If a form field has a placeholder attribute and no value,
the :placeholder-shown
pseudo-class can be used to attach styles to that state.
As soon as there is content in the field,
whether it has a placeholder or not,
this state will no longer apply.
Validation states
{% BrowserCompat 'css.selectors.valid' %}
You can respond to HTML form validation with pseudo-classes such as
:valid,
:invalid and
:in-range.
The :valid and :invalid pseudo-classes are useful for contexts
such as an email field that has a pattern that needs to be matched,
for it to be a valid field.
This valid value state can be shown to the user,
helping them understand they can safely move to the next field.
The :in-range pseudo-class is available if an input has a min and max,
such as a numeric input and the value is within those bounds.
With HTML forms,
you can determine that a field is required with the required attribute.
The :required
pseudo-class will be available for required fields.
Fields that are not required can be selected with the
:optional pseudo-class.
{% Aside %} It's not a good idea to rely solely on color to signify state changes— especially red and green—because colorblind and low-vision users can struggle to see a state change, or even miss it completely. A good idea is to use color to support state changes, along with text changes and icon changes to visually signify change {% endAside %}
Selecting elements by their index, order and occurrence
There is a group of pseudo-classes that select items based on where they are in the document.
:first-child and :last-child
{% BrowserCompat 'css.selectors.first-child' %}
If you want to find the first or last item,
you can use
:first-child and
:last-child.
These pseudo-classes will return either the first or last element in a group of sibling elements.
:only-child
{% BrowserCompat 'css.selectors.only-child' %}
You can also select elements that have no siblings,
with the
:only-child pseudo-class.
:first-of-type and :last-of-type
{% BrowserCompat 'css.selectors.first-of-type' %}
You can select the
:first-of-type and
:last-of-type which at first,
look like they do the same thing as :first-child and :last-child, but consider this HTML:
<div class="my-parent">
<p>A paragraph</p>
<div>A div</div>
<div>Another div</div>
</div>
And this CSS:
.my-parent div:first-child {
color: red;
}
No elements would be colored red because the first child is a paragraph and not a div.
The :first-of-type pseudo-class is useful in this context.
.my-parent div:first-of-type {
color: red;
}
Even though the first <div> is the second child,
it is still the first of type inside the .my-parent element,
so with this rule, it will be colored red.
:nth-child and :nth-of-type
{% BrowserCompat 'css.selectors.nth-child' %}
You're not limited to first and last children and types either.
The :nth-child and
:nth-of-type
pseudo-classes allow you to specify an element that is at a certain index.
The indexing in CSS selectors starts at 1.
You can pass more than an index into these pseudo-classes too.
If you wanted to select all even elements, you can use :nth-child(even).
You can also create more complex selectors that find items at regularly spaced intervals, using the An+B microsyntax.
li:nth-child(3n+3) {
background: yellow;
}
This selector selects every third item,
starting at item 3.
The n in this expression is the index,
which starts at zero the 3 (3n) is how much you multiply that index by.
Let's say you have 7 <li> items.
The first item that is selected is 3 because 3n+3 translates to (3 * 0) + 3.
The next iteration would pick item 6 because n has now incremented to 1,
so (3 * 1) + 3).
This expression works for both :nth-child and :nth-of-type.
You can play around with this sort of selector on this nth-child tester or this quantity selector tool.
:only-of-type
{% BrowserCompat 'css.selectors.only-of-type' %}
Lastly, you can find the only element of a certain type in a group of siblings with
:only-of-type.
This is useful if you want to select lists with only one item,
or if you want to find the only bold element in a paragraph.
Finding empty elements
It can sometimes be useful to identify completely empty elements, and there is a pseudo-class for that too.
:empty
{% BrowserCompat 'css.selectors.empty' %}
If an element has no children, the
:empty pseudo-class applies to them.
Children aren't just HTML elements or text nodes though: they can also be whitespace,
which can be confusing when you're debugging the following HTML and wondering why it isn't working with :empty:
<div>
</div>
The reason is that there's some whitespace between the opening and closing <div>,
so empty won't work.
The :empty pseudo-class can be useful if you have little control over the HTML and want to hide empty elements,
such as a WYSIWYG content editor.
Here, an editor has added a stray, empty paragraph.
<article class="post">
<p>Donec ullamcorper nulla non metus auctor fringilla.</p>
<p></p>
<p>Curabitur blandit tempus porttitor.</p>
</article>
With :empty, you can find that and hide it.
.post :empty {
display: none;
}
Finding and excluding multiple elements
Some pseudo-classes help you to write more compact CSS.
:is()
{% BrowserCompat 'css.selectors.is' %}
If you want to find all of the h2, li and img child elements in a .post element,
you might think to write a selector list like this:
.post h2,
.post li,
.post img {
…
}
With the :is()
pseudo-class, you can write a more compact version:
.post :is(h2, li, img) {
…
}
The :is pseudo-class is not only more compact than a selector list but it is also more forgiving.
In most cases,
if there's an error or unsupported selector in a selector list,
the entire selector list will no longer work.
If there's an error in the passed selectors in an :is pseudo-class,
it will ignore the invalid selector, but use those which are valid.
:not()
{% BrowserCompat 'css.selectors.not' %}
You can also exclude items with the
:not() pseudo-class.
For example, you can use it to style all links that don't have a class attribute.
a:not([class]) {
color: blue;
}
A :not pseudo-class can also help you to improve accessibility.
For example, an <img> must have an alt, even if its an empty value,
so you could write a CSS rule that adds a thick red outline to invalid images:
img:not([alt]) {
outline: 10px red;
}
{% Assessment 'pseudo-classes' %}
Pseudo-elements
If you've got an article of content and you want the first letter to be a much bigger drop cap— how do you achieve that?
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/Qpo2f3eRInt5iM6qW2p2.svg", alt="A couple of paragraphs of text with a blue drop cap", width="800", height="318" %}
In CSS,
you can use the ::first-letter pseudo-element to achieve this sort of design detail.
p::first-letter {
color: blue;
float: left;
font-size: 2.6em;
font-weight: bold;
line-height: 1;
margin-inline-end: 0.2rem;
}
A pseudo-element is like adding or targeting an extra element without having to add more HTML.
This example solution, using ::first-letter,
is one of many pseudo-elements.
They have a range of roles,
and in this lesson you're going to learn which pseudo-elements are available and how you can use them.
::before and ::after
Both the
::before and
::after
pseudo-elements create a child element inside an element only if you define a content property.
.my-element::before {
content: "";
}
.my-element::after {
content: "";
}
The content can be any string
—even an empty one—
but be mindful that anything other than an empty string will likely be announced by a screen reader.
You can add an image url,
which will insert an image at its original dimensions,
so you won't be able to resize it.
You can also insert a
counter.
{% Aside 'key-term' %} You can create a named counter and then increment it, based on its position in the document flow. There's all sorts of contexts where they can be really useful, such as automatically numbering an outline. {% endAside %}
Once a ::before or ::after element has been created,
you can style it however you want with no limits.
You can only insert a ::before or ::after element to an element that will accept child elements
(elements with a document tree),
so elements such as <img />, <video> and <input> won't work.
{% Aside 'gotchas' %}
input[type="checkbox"] is an exception.
It is allowed to have pseudo-element children.
{% endAside %}
::first-letter
We met this pseudo-element at the start of the lesson.
It is worth being aware that not all CSS properties can be used when targeting
::first-letter.
The available properties are:
colorbackgroundproperties (such asbackground-image)borderproperties (such asborder-color)floatfontproperties (such asfont-sizeandfont-weight)- text properties (such as
text-decorationandword-spacing)
p::first-letter {
color: goldenrod;
font-weight: bold;
}
{% Aside %}
You can only use :first-letter on block containers.
Therefore, it won't work if you try to add it to an element that has display: inline.
{% endAside %}
::first-line
The ::first-line
pseudo-element will let you style the first line of text
only if the element with ::first-line applied has a display value of block,
inline-block, list-item, table-caption or table-cell.
p::first-line {
color: goldenrod;
font-weight: bold;
}
Like the ::first-letter pseudo-element,
there's only a subset of CSS properties you can use:
colorbackgroundpropertiesfontpropertiestextproperties
::backdrop
If you have an element that is presented in full screen mode,
such as a <dialog> or a <video>,
you can style the backdrop—the space between the element and the rest of the page—with the
::backdrop pseudo-element:
video::backdrop {
background-color: goldenrod;
}
The ::backdrop pseudo-element is supported in all major browsers apart from Safari.
::marker
The ::marker
pseudo-element lets you style the bullet or number for a list item or the arrow of a <summary> element.
::marker {
color: hotpink;
}
ul ::marker {
font-size: 1.5em;
}
ol ::marker {
font-size: 1.1em;
}
summary::marker {
content: '\002B'' '; /* Plus symbol with space */
}
details[open] summary::marker {
content: '\2212'' '; /* Minus symbol with space */
}
Only a small subset of CSS properties are supported for ::marker:
colorcontentwhite-spacefontpropertiesanimationandtransitionproperties
You can change the marker symbol, using the content property. You can use this to set a plus and minus symbol for the closed and empty states of a <summary> element, for example.
::selection
The ::selection
pseudo-element allows you to style how selected text looks.
::selection {
background: green;
color: white;
}
This pseudo-element can be used to style all selected text as in the above demo. It can also be used in combination with other selectors for a more specific selection style.
p:nth-of-type(2)::selection {
background: darkblue;
color: yellow;
}
As with other pseudo-elements, only a subset of CSS properties are allowed:
colorbackground-colorbut notbackground-imagetextproperties
::placeholder
{% BrowserCompat 'css.selectors.placeholder' %}
You can add a helper hint to form elements,
such as <input> with a placeholder attribute.
The ::placeholder
pseudo-element allows you to style that text.
input::placeholder {
color: darkcyan;
}
The ::placeholder only supports a subset of CSS rules:
colorbackgroundpropertiesfontpropertiestextproperties
{% Aside %}
A placeholder is not <label>
and should not be used in place of a <label>.
Form elements must be labelled or they will be inaccessible.
{% endAside %}
::cue
{% BrowserCompat 'css.selectors.cue' %}
Last in this tour of pseudo-elements is the
::cue pseudo-element.
This allows you to style the WebVTT cues,
which are the captions of a <video> element.
You can also pass a selector into a ::cue,
which allows you to style specific elements inside a caption.
video::cue {
color: yellow;
}
video::cue(b) {
color: red;
}
video::cue(i) {
color: lightpink;
}
{% Assessment 'pseudo-elements' %}
Selectors
If you've got some text that you only want to be larger and red if it's the first paragraph of an article, how do you do that?
<article>
<p>I want to be red and larger than the other text.</p>
<p>I want to be normal sized and the default color.</p>
</article>
You use a CSS selector to find that specific element and apply a CSS rule, like this.
article p:first-of-type {
color: red;
font-size: 1.5em;
}
CSS provides you with a lot of options to select elements and apply rules to them, ranging from very simple to very complex, to help solve situations like this.
The parts of a CSS rule
To understand how selectors work and their role in CSS, it's important to know the parts of a CSS rule. A CSS rule is a block of code, containing one or more selectors and one or more declarations.
In this CSS rule, the selector is .my-css-rule
which finds all elements with a class of my-css-rule on the page.
There are three declarations within the curly brackets.
A declaration is a property and value pair which applies styles to the elements matched by the selectors.
A CSS rule can have as many declarations and selectors as you like.
Simple selectors
The most straightforward group of selectors target HTML elements plus classes, IDs, and other attributes which may be added to an HTML tag.
Universal selector
{% BrowserCompat 'css.selectors.universal' %}
A universal selector—also known as a wildcard—matches any element.
* {
color: hotpink;
}
This rule causes every HTML element on the page to have hotpink text.
Type selector
{% BrowserCompat 'css.selectors.type' %}
A type selector matches a HTML element directly.
section {
padding: 2em;
}
This rule causes every <section> element to have 2em of padding on all sides.
Class selector
{% BrowserCompat 'css.selectors.class' %}
A HTML element can have one or more items defined in their class attribute.
The
class selector
matches any element that has that class applied to it.
<div class="my-class"></div>
<button class="my-class"></button>
<p class="my-class"></p>
Any element that has the class applied to it will get colored red:
.my-class {
color: red;
}
Notice how the . is only present in CSS and not the HTML.
This is because the . character instructs the CSS language to match class attribute members.
This is a common pattern in CSS, where a special character,
or set of characters, is used to define selector types.
A HTML element that has a class of .my-class will still be matched to the above CSS rule,
even if they have several other classes, like this:
<div class="my-class another-class some-other-class"></div>
This is because CSS looks for a class attribute that contains the defined class,
rather than matching that class exactly.
{% Aside %}
The value of a class attribute can be almost anything you want it to be.
One thing that could trip you up, is that you can't start a class (or an ID) with a number,
such as .1element.
You can read more in the specification.
{% endAside %}
ID selector
{% BrowserCompat 'css.selectors.id' %}
An HTML element with an id attribute should be the only element on a page with that ID value.
You select elements with an
ID selector like this:
#rad {
border: 1px solid blue;
}
This CSS would apply a blue border to the HTML element that has an id of rad, like this:
<div id="rad"></div>
Similarly to the class selector .,
use a # character to instruct CSS to look for an element that matches the id that follows it.
{% Aside %}
If the browser encounters more than one instance of an id
it will still apply any CSS rules that match its selector.
However, any element that has an id attribute is supposed to have a unique value for it,
so unless you're writing very specific CSS for a single element,
avoid applying styles with the id selector as it means you can't re-use those styles elsewhere.
{% endAside %}
Attribute selector
{% BrowserCompat 'css.selectors.attribute' %}
You can look for elements that have a certain HTML attribute,
or have a certain value for an HTML attribute,
using the attribute selector.
Instruct CSS to look for attributes by wrapping the selector with square brackets ([ ]).
[data-type='primary'] {
color: red;
}
This CSS looks for all elements that have an attribute of data-type with a value of primary, like this:
<div data-type="primary"></div>
Instead of looking for a specific value of data-type,
you can also look for elements with the attribute present, regardless of its value.
[data-type] {
color: red;
}
<div data-type="primary"></div>
<div data-type="secondary"></div>
Both of these <div> elements will have red text.
You can use case-sensitive attribute selectors
by adding an s operator to your attribute selector.
[data-type='primary' s] {
color: red;
}
This means that if a HTML element had a data-type of Primary,
instead of primary, it would not get red text.
You can do the opposite—case insensitivity—by using an i operator.
Along with case operators, you have access to operators that match portions of strings inside attribute values.
/* A href that contains "example.com" */
[href*='example.com'] {
color: red;
}
/* A href that starts with https */
[href^='https'] {
color: green;
}
/* A href that ends with .com */
[href$='.com'] {
color: blue;
}
Grouping selectors
A selector doesn't have to match only a single element. You can group multiple selectors by separating them with commas:
strong,
em,
.my-class,
[lang] {
color: red;
}
This example extends the color change to both <strong> elements and <em> elements.
It's also extended to a class named .my-class, and an element that has a lang attribute.
{% Assessment 'simple-selectors' %}
Pseudo-classes and pseudo-elements
CSS provides useful selector types that focus on specific platform state, like when an element is hovered, structures inside an element, or parts of an element.
Pseudo-classes
HTML elements find themselves in various states, either because they are interacted with, or one of their child elements is in a certain state.
For example, an HTML element could be hovered with the mouse pointer by a user
or a child element could also be hovered by the user.
For those situations, use the :hover pseudo-class.
/* Our link is hovered */
a:hover {
outline: 1px dotted green;
}
/* Sets all even paragraphs to have a different background */
p:nth-child(even) {
background: floralwhite;
}
Find out more in the pseudo-classes module.
Pseudo-element
Pseudo-elements differ from pseudo-classes because instead of responding to the platform state,
they act as if they are inserting a new element with CSS.
Pseudo-elements are also syntactically different from pseudo-classes,
because instead of using a single colon (:), we use a double colon (::).
{% Aside %}
A double colon (::) is what distinguishes a pseudo-element from a pseudo-class,
but because this distinction wasn't present in older versions of CSS specs,
browsers support a single colon for the original pseudo-elements,
such as :before and :after to help with backwards compatibility with older browsers, like IE8.
{% endAside %}
.my-element::before {
content: 'Prefix - ';
}
As in the above demo where you prefixed a link's label with the type of file it was,
you can use the ::before pseudo-element to insert content at the start of an element,
or the ::after pseudo-element to insert content at the end of an element.
Pseudo-elements aren't limited to inserting content, though.
You can also use them to target specific parts of an element.
For example, suppose you have a list.
Use ::marker to style each bullet point (or number) in the list
/* Your list will now either have red dots, or red numbers */
li::marker {
color: red;
}
You can also use ::selection to style the content that has been highlighted by a user.
::selection {
background: black;
color: white;
}
Learn more in the module on pseudo-elements.
{% Assessment 'pseudo-selectors' %}
Complex selectors
You have already seen a vast array of selectors, but sometimes, you will need more fine-grained control with your CSS. This is where complex selectors step in to help.
It's worth remembering at this point that although the following selectors give us more power, we can only cascade downwards, selecting child elements. We are not able to target upwards and select a parent element. We cover what the cascade is and how it works in a later lesson.
Combinators
A combinator is what sits between two selectors.
For example, if the selector was p > strong, the combinator is the > character.
The selectors which use these combinators help you select items based on their position in the document.
Descendant combinator
To understand descendant combinators, you need to first understand parent and child elements.
<p>A paragraph of text with some <strong>bold text for emphasis</strong>.</p>
The parent element is the <p> which contains text.
Inside that <p> element is a <strong> element, making its content bold.
Because it is inside the <p>, it is a child element.
A descendant combinator allows us to target a child element.
This uses a space ( ) to instruct the browser to look for child elements:
p strong {
color: blue;
}
This snippet selects all <strong> elements
that are child elements of <p> elements only, making them blue recursively.
This effect is better visualised in the above example,
using the combinator selector, .top div.
That CSS rule adds left padding to those <div> elements.
Because the combinator is recursive,
all <div> elements that are in .top will have that same padding applied to them.
Take a look at the HTML panel in this demo
to see how the .top element has several <div> child elements which themselves,
have <div> child elements.
Next sibling combinator
You can look for an element that immediately follows another element
by using a + character in your selector.
To add space between stacked elements,
use the next sibling combinator to add space
only if an element is a next sibling of a child element of .top.
You could add margin to all child elements of .top,
using the following selector:
.top * {
margin-top: 1em;
}
The problem with this is that because you're selecting every child element of .top,
this rule potentially creates extra, unnecessary space.
The next sibling combinator,
mixed with a universal selector enables you to not only control what elements get space,
but also apply space to any element.
This provides you with some long-term flexibility,
regardless of what HTML elements appear in .top.
Subsequent- sibling combinator
A subsequent combinator is very similar to a next sibling selector.
Instead of a + character however,
use a ~ character.
How this differs is that an element just has to follow another element with the same parent,
rather than being the next element with the same parent.
This subsequent combinator provides a little less rigidity,
which is useful in contexts like the above sample,
where we change the color of a custom switch when its associated checkbox has the :checked state.
Child combinator
A child combinator (also known as direct descendant)
allows you more control over the recursion that comes with combinator selectors.
By using the > character, you limit the combinator selector to apply only to direct children.
Consider the previous, next sibling selector example. The space is added to each next sibling, but if one of those elements also has next sibling elements as children, it can result in undesirable, extra spacing.
To alleviate this problem,
change the next sibling selector to incorporate a child combinator: > * + *.
The rule will now only apply to direct children of .top.
Compound selectors
You can combine selectors to increase specificity and readability.
For example, to target <a> elements,
that also have a class of .my-class, write the following:
a.my-class {
color: red;
}
This wouldn't apply a red color to all links
and it would also only apply the red color to .my-class if it was on an <a> element.
For more on this, see the specificity module.
{% Assessment 'complex-selectors' %}
Resources
- CSS selectors reference
- Interactive selectors game
- Pseudo-class and pseudo-elements reference
- A tool that translates CSS selectors into plain-english explainers
Shadows
Say you've been sent a design to build and in that design there's a picture of a t-shirt, cut out, with a drop shadow. The designer tells you that the product image is dynamic and can be updated via the content management system, so the drop shadow needs to be dynamic too. Instead of a t-shirt, the image could be a visor or shorts, or any other item. How do you do that with CSS?
CSS has the
box-shadow and
text-shadow properties,
but the picture isn't text, so you can't use text-shadow.
If you use box-shadow, the shadow is on the surrounding box,
not around the t-shirt.
Luckily, there is another option: the
drop-shadow() filter.
This enables you to do exactly what the designer asked for.
There are plenty of options when it comes to shadows in CSS,
each designed for a different use case.
Box shadow
{% BrowserCompat 'css.properties.box-shadow' %}
The box-shadow property is for adding shadows to the box of an HTML element.
It works on block elements and inline elements.
.my-element {
box-shadow: 5px 5px 20px 5px #000;
}
The order of values for box-shadow are as follows:
- Horizontal offset: a positive number pushes it out from the left and a negative number will push it out from the right.
- Vertical offset: a positive number pushes it down from the top, and a negative number will push it up from the bottom.
- Blur radius: a larger number produces a more blurred shadow, whereas a small number produces a sharper shadow.
- Spread radius (optional): a larger number increases the size of the shadow and a smaller number decreases it, making it the same size as the blur radius if it's set to 0.
- Color: Any valid color value. If this isn't defined, the computed text color will be used.
To make a box shadow an inner shadow,
rather than the default outer shadow,
add an inset keyword before the other properties.
/* Outer shadow */
.my-element {
box-shadow: 5px 5px 20px 5px #000;
}
/* Inner shadow */
.my-element {
box-shadow: inset 5px 5px 20px 5px #000;
}
Multiple shadows
You can add as many shadows as you like with box-shadow.
Add a comma separated collection of value sets to achieve this:
.my-element {
box-shadow: 5px 5px 20px 5px darkslateblue, -5px -5px 20px 5px dodgerblue,
inset 0px 0px 10px 2px darkslategray, inset 0px 0px 20px 10px steelblue;
}
Properties affecting box-shadow
Adding a border-radius to your box will also affect the shape of the box shadow.
This is because CSS is creating a shadow based on the shape of the box
as if light is pointing at it.
.my-element {
box-shadow: 0px 0px 20px 5px darkslateblue;
border-radius: 25px;
}
If your box with box-shadow is in a container that has overflow: hidden,
the shadow won't break out of that overflow either.
<div class="my-parent">
<div class="my-shadow">My shadow is hidden by my parent.</div>
</div>
.my-parent,
.my-shadow {
width: 250px;
height: 250px;
}
.my-shadow {
box-shadow: 0px 0px 20px 5px darkslateblue;
}
.my-parent {
overflow: hidden;
}
Text shadow
{% BrowserCompat 'css.properties.text-shadow' %}
The text-shadow property is very similar to the box-shadow property.
It only works on text nodes.
.my-element {
text-shadow: 3px 3px 3px hotpink;
}
The values for text-shadow are the same as box-shadow and in the same order.
The only difference is that text-shadow has no spread value and no inset keyword.
When you add a box-shadow it is clipped to the shape of your box,
but text-shadow has no clipping.
This means that if your text is fully or semi transparent,
the shadow is visible through it.
.my-element {
text-shadow: 3px 3px 3px gold;
color: rgb(0 0 0 / 70%);
}
Multiple shadows
You can add as many shadows as you like with text-shadow,
just as with box-shadow.
Add a comma separated collection of value sets,
and you can create some really cool text effects, such as 3D text.
.my-element {
text-shadow: 1px 1px 0px white,
2px 2px 0px firebrick;
color: darkslategray;
}
Drop shadow
{% BrowserCompat 'css.properties.drop-shadow' %}
To achieve a drop shadow that follows any potential curves of an image,
use the CSS drop-shadow filter.
This shadow is applied to an alpha mask which makes it very useful for adding a shadow to a cutout image,
as in the case in the intro of this module.
.my-image {
filter: drop-shadow(0px 0px 10px rgba(0 0 0 / 30%))
}
{% Aside 'key-term' %} We cover CSS filters in another module, but in short, filters allow you to apply multiple graphical effects to the pixels of an element. {% endAside %}
The drop-shadow filter has the same values as box-shadow but the inset keyword and spread value are not allowed. You can add as many shadows as you like,
by adding multiple instances of drop-shadow values to the filter property.
Each shadow will use the last shadow as a positioning reference point.
{% Assessment 'shadows' %}
Sizing Units
The web is a responsive medium, but sometimes you want to control its dimensions to improve the overall interface quality. A good example of this is limiting line lengths to improve readability. How would you do that in a flexible medium like the web?
For this case,
you can use a ch unit, which is equal to the width of a "0" character in the rendered font at its computed size.
This unit allows you to limit the width of text with a unit that's designed to size text,
which in turn,
allows predictable control regardless of the size of that text.
The ch unit is one of a handful of units that are helpful for specific contexts like this example.
Numbers
Numbers are used to define opacity, line-height and even for color channel values in rgb.
Numbers are unitless integers (1, 2, 3, 100) and decimals (.1, .2, .3).
Numbers have meaning depending on their context.
For example, when defining line-height,
a number is representative of a ratio if you define it without a supporting unit:
p {
font-size: 24px;
line-height: 1.5;
}
In this example, 1.5 is equal to 150% of the p element's computed pixel font size.
This means that if the p has a font-size of 24px,
the line height will be computed as 36px.
{% Aside %}
It's a good idea to use a unitless value for line-height,
rather than specifying a unit.
As you learned in the inheritance module,
font-size can be inherited.
Defining a unitless line-height keeps the line-height relative to the font size.
This provides a better experience than, say, line-height: 15px,
which will not change and might look strange with certain font sizes.
{% endAside %}
Numbers can also be used in the following places:
- When setting values for filters:
filter: sepia(0.5)applies a50%sepia filter to the element. - When setting opacity:
opacity: 0.5applies a50%opacity. - In color channels:
rgb(50, 50, 50), where the values 0-255 are acceptable to set a color value. See color lesson. - To transform an element:
transform: scale(1.2)scales the element by 120% of its initial size.
Percentages
When using a percentage in CSS you need to know how the percentage is calculated.
For example,width is calculated as a percentage of the available width in the parent element.
div {
width: 300px;
height: 100px;
}
div p {
width: 50%;
}
In the above example, the width of div p is 150px, assuming that the layout uses the default box-sizing: content-box.
If you set margin or padding as a percentage,
they will be a portion of the parent element's width,
regardless of direction.
div {
width: 300px;
height: 100px;
}
div p {
margin-top: 50%; /* calculated: 150px */
padding-left: 50%; /* calculated: 150px */
}
In the above snippet, both the margin-top and padding-left will compute to 150px.
div {
width: 300px;
height: 100px;
}
div p {
width: 50%; /* calculated: 150px */
transform: translateX(10%); /* calculated: 15px */
}
If you set a transform value as a percentage,
it is based on the element with the transform set.
In this example, the p has a translateX value of 10% and a width of 50%.
First, calculate what the width will be: 150px because it is 50% of its parent's width.
Then, take 10% of 150px, which is 15px.
{% Aside 'key-term' %} The transform property allows you alter an element's appearance and position by rotating, skewing, scaling and translating it. This can be done in a 2D and 3D space. {% endAside %}
Dimensions and lengths
If you attach a unit to a number, it becomes a dimension.
For example, 1rem is a dimension.
In this context, the unit that is attached to a number is referred to in specifications as a dimension token.
Lengths are dimensions that refer to distance and they can either be absolute or relative.
Absolute lengths
All absolute lengths resolve against the same base,
making them predictable wherever they're used in your CSS.
For example, if you use cm to size your element and then print,
it should be accurate if you compared it to a ruler.
div {
width: 10cm;
height: 5cm;
background: black;
}
If you printed this page, the div would print as a 10x5cm black rectangle.
Keep in mind, CSS is used not only for digital content, but also to style print content.
Absolute lengths can really come in handy when designing for print.
| Unit | Name | Equivalent to |
|---|---|---|
| cm | Centimeters | 1cm = 96px/2.54 |
| mm | Millimeters | 1mm = 1/10th of 1cm |
| Q | Quarter-millimeters | 1Q = 1/40th of 1cm |
| in | Inches | 1in = 2.54cm = 96px |
| pc | Picas | 1pc = 1/6th of 1in |
| pt | Points | 1pt = 1/72th of 1in |
| px | Pixels | 1px = 1/96th of 1in |
Relative lengths
A relative length is calculated against a base value, much like a percentage.
The difference between these and percentages is that you can contextually size elements.
This means that CSS has units such as ch that use the text size as a basis,
and vw which is based on the width of the viewport (your browser window).
Relative lengths are particularly useful on the web due to its responsive nature.
Font-size-relative units
CSS provides helpful units that are relative to the size of elements of rendered typography,
such as the size of the text itself (em units) or width of the typefaces characters (ch units).
| unit | relative to: |
|---|---|
| em | Relative to the font size, i.e. 1.5em will be 50% larger than the base computed font size of its parent. (Historically, the height of the capital letter "M"). |
| ex | Heuristic to determine whether to use the x-height, a letter "x", or `.5em` in the current computed font size of the element. |
| cap | Height of the capital letters in the current computed font size of the element. |
| ch | Average character advance of a narrow glyph in the element's font (represented by the "0" glyph). |
| ic | Average character advance of a full width glyph in the element's font, as represented by the "水" (CJK water ideograph, U+6C34) glyph. |
| rem | Font size of the root element (default is 16px). |
| lh | Line height of the element. |
| rlh | Line height of the root element. |
Viewport-relative units
You can use the dimensions of the viewport (browser window) as a relative basis. These units portion up the available viewport space.
| unit | relative to |
|---|---|
| vw | 1% of viewport's width. People use this unit to do cool font tricks, like resizing a header font based on the width of the page so as the user resizes, the font will also resize. |
| vh | 1% of viewport's height. You can use this to arrange items in a UI, if you have a footer toolbar for example. |
| vi | 1% of viewport's size in the root element's inline axis. Axis refers to writing modes. In horizontal writing modes like English, the inline axis is horizontal. In vertical writing modes like some Japanese typefaces, the inline axis runs top to bottom. |
| vb | 1% of viewport's size in the root element's block axis. For the block axis, this would be the directionality of the language. LTR languages like English would have a vertical block axis, since English language readers parse the page from top to bottom. A vertical writing mode has a horizontal block axis. |
| vmin | 1% of the viewport's smaller dimension. |
| vmax | 1% of the viewport's larger dimension. |
div {
width: 10vw;
}
p {
max-width: 60ch;
}
In this example, the div will be 10% of the viewport's width because 1vw is 1% of the viewport width.
The p element has a max-width of 60ch
which means it can't exceed the width of 60 "0" characters in the calculated font and size.
{% Aside 'objective' %}
By sizing text with relative units like em or rem,
rather than an absolute unit, like px, the size of your text can respond to user preferences.
This can include the system font size or parent element's font size, such as the <body>.
The base size of the em is the element's parent and the base size of the rem is the base font size of the document.
If you don't define a font-size on your html element,
this user-preferred system font size will be honoured if you use relative lengths,
such as em and rem.
If you use px units for sizing text,
this preference will be ignored.
{% Video src="video/VbAJIREinuYvovrBzzvEyZOpw5w1/cUEl77VN4ZtAdElV4fd6.mp4", autoplay=true, controls=true, muted=true %}
{% endAside %}
Miscellaneous units
There are some other units which have been specified to deal with particular types of values.
Angle units
In the color module,
we looked at angle units,
which are helpful for defining degree values,
such as the hue in hsl.
They are also useful for rotating elements within transform functions.
div {
width: 150px;
height: 150px;
transform: rotate(60deg);
}
Using the deg angle unit, you can rotate a div 90° on its center axis.
div {
background-image: url('a-low-resolution-image.jpg');
}
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
div {
background-image: url('a-high-resolution-image.jpg');
}
}
{% Aside %}
Other angle units include rad (radians), grad (gradians), and turn units,
which represent a part of an angle, where 1turn = 360deg, and 0.5turn = 180deg.
{% endAside %}
Resolution units
In the previous example the value of min-resolution is 192dpi.
The dpi unit stands for dots per inch.
A useful context for this is detecting very high resolution screens,
such as Retina displays in a media query and serving up a higher resolution image.
{% Assessment 'sizing' %}
Resources
Spacing
Say you've got a collection of three boxes, stacked on top of each other and you want space between them. How many ways can you think of to do that in CSS?
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/jGjBqBRk3uCvXT7Tdjre.svg", alt="Three stacked boxes with a downward arrow", width="800", height="498" %}
The margin property might give you what you need,
but it also might add additional spacing that you don't want.
For example, how do you target just the space in between each of those elements?
Something like gap might be more appropriate in this case.
There are many ways to adjust spacing within a UI,
each with its own strengths and caveats.
HTML spacing
HTML itself provides some methods to space elements.
The <br> and <hr> elements allow you to space elements in the block direction,
which if you are in a latin-based language,
is top-to-bottom.
If you use a <br> element,
it will create a line-break,
just like if you were to press your enter key in a word processor.
The <hr> creates a horizontal line with space either-side, known as margin.
Along with using HTML elements,
HTML entities can create space.
An HTML entity is a reserved string of characters that are replaced with character entities by the browser.
For example,
if you were to type © in your HTML file,
it would be converted into a © character.
The entity is converted into a non-breaking space character,
which provides an inline space.
Be careful though,
because the non-breaking aspect of this character stitches the two elements together,
which can result in odd behaviour.
{% Aside %}
Use HTML elements to add space only when the element helps with the understanding of the document.
For example, an <hr> doesn't just add space,
it creates a logical separation of two chunks of content.
If you just want a line with space around it,
adding a border with CSS might be more appropriate.
{% endAside %}
Margin
If you want to add space to the outside of an element,
you use the margin property.
Margin is like adding a cushion around your element.
The margin property is shorthand for margin-top,
margin-right, margin-bottom and margin-left.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/ECuEOJEGnudhXW5JEFih.svg", alt="A diagram of the four main areas of the box model.", width="800", height="547" %}
The margin shorthand applies properties in a particular order:
top, right, bottom and left.
You can remember these with trouble: TRouBLe.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/F8jtvl15CsAcs8Oaid2l.svg", alt="The word 'Trouble' running downwards with T, R, B and L extending to Top, Right, Bottom and Left. A box with arrows showing the directions too.", width="800", height="320" %}
The margin shorthand can also be used with one, two, or three values.
Adding a fourth value lets you set each individual side.
These are applied as follows:
- One value will be applied to all sides. (
margin: 20px). - Two values: the first value will be applied to the top and bottom sides,
and the second value will be applied to the left and right sides.
(
margin: 20px 40px) - Three values: the first value is
top, the second value isleftandright, and the third value isbottom. (margin: 20px 40px 30px).
Margin can be defined with a length,
percentage or auto value, such as 1em or 20%.
If you use a percentage,
the value will be calculated based on the width of your element's containing block.
This means that if your element's containing block has a width of 250px
and your element has a margin value of 20%:
each side of your element will have a computed margin of 50px.
You can also use a value of auto for margin.
For block level elements with a restricted size,
an auto margin will take up available space in the direction that it is applied to.
A good example is this one,
from the flexbox module, where the items push away from each other.
Another good example of auto margin is a horizontally centered wrapper which has a max width.
This sort of wrapper is often used to create a consistent center column on a website.
.wrapper {
max-width: 400px;
margin: 0 auto;
}
Here, margin is removed from the top and bottom (block) sides,
and auto shares the space between the left and right (inline) sides.
{% Aside %}
In the previous module on logical properties,
you learned that instead of specifying margin-top, margin-right, margin-bottom and margin-left,
you can use margin-block-start, margin-inline-end, margin-block-end and margin-inline-start.
{% endAside %}
Negative margin
Negative values can also be used for margin. Instead of adding space between adjacent sibling elements, it will reduce space between them. This can result in overlapping elements, if you declare a negative value that's more than the available space.
Margin collapse
Margin collapse is a tricky concept, but it's something you'll run into very commonly when building interfaces. Say you have two elements: a heading and a paragraph that both have vertical margin on them:
<article>
<h1>My heading with teal margin</h1>
<p>A paragraph of text that has blue margin on it, following the heading with margin.</p>
</article>
h1 {
margin-bottom: 2rem;
}
p {
margin-top: 3rem;
}
At first glance,
you would be forgiven for thinking that the paragraph will be spaced 5em from the heading,
because 2rem and 3rem combined calculate to 5rem.
Because vertical margin collapses, though, the space is actually 3rem.
Margin collapse works by selecting the largest value of two adjoining elements
with vertical margin set on the adjoining sides.
The bottom of the h1 meets the top of the p,
so the largest value of the h1's bottom margin and the p's top margin is selected.
If the h1 were to have 3.5rem of bottom margin,
the space between them both would then be 3.5rem because that is larger than 3rem.
Only block margins collapse, not inline (horizontal) margins.
{% Aside %} This behavior is rooted back to when the web was mostly just documents. Collapsing margins help to set consistent spacing between elements without accidentally creating huge gaps between elements that also have margin defined. {% endAside %}
Margin collapse also helps with empty elements.
If you have a paragraph that has a top and bottom margin of 20px,
it will only create 20px of space: not 40px.
If anything is added to the inside of this element though,
including padding, its margin will no longer collapse in itself and will be treated as any box with content.
{% Assessment 'margin-collapsing' %}
Preventing margin collapse
If you make an element absolutely positioned,
using position: absolute, the margin will no longer collapse.
The margin also won't collapse if you use the float property, too.
If you have an element with no margin between two elements with block margin, the margin won't collapse either, because the two elements with block margin are no longer adjacent siblings: they are just siblings.
In the layout lesson, you learned that flexbox and grid containers are very similar to block containers, but handle their child elements very differently. This is the case with margin collapse, too.
If we take the original example from the lesson and apply flexbox with column direction, the margins are combined, instead of collapsed. This can provide predictability with layout work, which is what flexbox and grid containers are designed for.
Margin and margin collapse can be tricky to understand, but understanding how they work, in detail, is very useful, so this detailed explainer is strongly recommended.
Padding
Instead of creating space on the outside of your box,
like margin does,
the padding property creates space on the inside of your box instead:
like insulation.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/kClqXks3zC6Dio1c6f6v.png", alt="A box with arrows pointing inwards to show that padding lives inside a box", width="800", height="336" %}
Depending on which box model you are using—which was covered back in the
box model lesson
—padding can also affect the overall dimensions of the element too.
The padding property is shorthand for padding-top, padding-right, padding-bottom and padding-left.
Just like margin, padding has logical properties, too:
padding-block-start, padding-inline-end, padding-block-end and padding-inline-start.
Positioning
Also covered in the layout module,
if you set a value for position that is anything other than static,
you can space elements with the top, right, bottom and left properties.
There are some differences with how these directional values behave:
- An element with
position: relativewill maintain its place in the document flow, even when you set these values. They will be relative to your element's position too. - An element with
position: absolutewill base the directional values from the relative parent's position. - An element with
position: fixedwill base the directional values on the viewport. - An element with
position: stickywill only apply the directional values when it is in its docked/stuck state.
In the logical properties module,
you learn about the inset-block and inset-inline properties,
which allow you to set directional values that honor writing mode.
Both properties are shorthands combining the start and end values
and as such accept either one value to be set for start and end or
two individual values.
Grid and flexbox
Lastly, in both grid and flexbox you can use the gap property to create space between child elements.
The gap property is shorthand for row-gap and column-gap,
it accepts one or two values, which can be lengths or percentages.
You can also use keywords such as unset, initial and inherit.
If you define only one value,
the same gap will be applied to both the rows and columns,
but if you define both values,
the first value is row-gap and the second value is column-gap.
With both flexbox and grid, you can also create space using their distribution and alignment capabilities, which we cover in the grid module and flexbox module.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/JNXSRH4j77loSB099E04.svg", alt="A diagram representation of a grid with gaps", width="800", height="434" %}
Creating consistent spacing
It is a really good idea to choose a strategy and stick with it to help you create a consistent user interface that has good flow and rhythm. A good way to achieve this is use consistent measures for your spacing.
For example, you could commit to using 20px
as a consistent measure for all gaps between elements—known as gutters—so
all layouts look and feel consistent.
You could also decide to use 1em as the vertical spacing between flow content,
which would achieve consistent spacing based on the element's font-size.
Whatever you choose,
you should save these values as variables (or CSS custom properties)
to tokenize those values and make the consistency a bit easier.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/8eun6aGNHPTr4kyOrYkc.svg", alt="Consistent spacing between elements, using either 20px for a layout or 1em for flow content.", width="800", height="602" %}
:root {
--gutter: 20px;
--spacing: 1em;
}
h1 {
margin-left: var(--gutter);
margin-top: var(--spacing);
}
Using custom properties like this allows you to define them once, then use them throughout your CSS. When they are updated, either locally within an element or globally, the values will pass down through the cascade and the updated values will be reflected.
{% Assessment 'conclusion' %}
Specificity
Suppose that you're working with the following HTML and CSS:
<button class="branding">Hello, Specificity!</button>
button {
color: red;
}
.branding {
color: blue;
}
There's two competing rules here. One will color the button red and the other will color it blue. Which rule gets applied to the element? Understanding the CSS specification's algorithm about specificity is the key to understanding how CSS decides between competing rules.
Specificity is one of the four distinct stages of the cascade, which was covered in the last module, on the cascade.
Specificity scoring
Each selector rule gets a scoring. You can think of specificity as a total score and each selector type earns points towards that score. The selector with the highest score wins.
With specificity in a real project, the balancing act is making sure the CSS rules you expect to apply, actually do apply, while generally keeping scores low to prevent complexity. The score should only be as high as we need it to be, rather than aiming for the highest score possible. In the future, some genuinely more important CSS might need to be applied. If you go for the highest score, you'll make that job hard.
Scoring each selector type
Each selector type earns points. You add all of these points up to calculate a selector's overall specificity.
Universal selector
A universal selector (*)
has no specificity and gets 0 points.
This means that any rule with 1 or more points will override it
* {
color: red;
}
Element or pseudo-element selector
An element (type) or pseudo-element selector gets 1 point of specificity .
Type selector
div {
color: red;
}
Pseudo-element selector
::selection {
color: red;
}
Class, pseudo-class, or attribute selector
A class, pseudo-class or attribute selector gets 10 points of specificity.
Class selector
.my-class {
color: red;
}
Pseudo-class selector
:hover {
color: red;
}
Attribute selector
[href='#'] {
color: red;
}
The :not()
pseudo-class itself adds nothing to the specificity calculation.
However, the selectors passed in as arguments do get added to the specificity calculation.
div:not(.my-class) {
color: red;
}
This sample would have 11 points of specificity
because it has one type selector (div) and one class inside the :not().
ID selector
An ID
selector gets 100 points of specificity,
as long as you use an ID selector (#myID) and not an attribute selector ([id="myID"]).
#myID {
color: red;
}
Inline style attribute
CSS applied directly to the style attribute of the HTML element,
gets a specificity score of 1,000 points.
This means that in order to override it in CSS,
you have to write an extremely specific selector.
<div style="color: red"></div>
!important rule
Lastly, an !important at the end of a CSS value gets a specificity score of 10,000 points.
This is the highest specificity that one individual item can get.
An !important rule is applied to a CSS property,
so everything in the overall rule (selector and properties) does not get the same specificity score.
.my-class {
color: red !important; /* 10,000 points */
background: white; /* 10 points */
}
{% Assessment 'scoring-beginner' %}
Specificity in context
The specificity of each selector that matches an element is added together. Consider this example HTML:
<a class="my-class another-class" href="#">A link</a>
This link has two classes on it. Add the following CSS, and it gets 1 point of specificity:
a {
color: red;
}
Reference one of the classes in this rule, it now has 11 points of specificity:
a.my-class {
color: green;
}
Add the other class to the selector, it now has 21 points of specificity:
a.my-class.another-class {
color: rebeccapurple;
}
Add the href attribute to the selector,
it now has 31 points of specificity:
a.my-class.another-class[href] {
color: goldenrod;
}
Finally,add a :hover pseudo-class to all of that,
the selector ends up with 41 points of specificity:
a.my-class.another-class[href]:hover {
color: lightgrey;
}
{% Assessment 'scoring-advanced' %}
Visualizing specificity
In diagrams and specificity calculators, the specificity is often visualized like this:
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/McrFhjqHXMznUzXbRuJ6.svg", alt="A diagram demonstrating most specific to least specific selectors", width="800", height="474" %}
The left group is id selectors.
The second group is class, attribute, and pseudo-class selectors.
The final group is element and pseudo-element selectors.
For reference, the following selector is 0-4-1:
a.my-class.another-class[href]:hover {
color: lightgrey;
}
{% Assessment 'visualizing' %}
Pragmatically increasing specificity
Let's say we have some CSS that looks like this:
.my-button {
background: blue;
}
button[onclick] {
background: grey;
}
With HTML that looks like this:
<button class="my-button" onclick="alert('hello')">Click me</button>
The previous rule—.my-button—gets 10 points (0-1-0),
because it has one class selector.
If you want to give this rule a boost, repeat the class selector like this:
.my-button.my-button {
background: blue;
}
button[onclick] {
background: grey;
}
{% Aside 'caution' %} If you find that you are needing to boost specificity like this frequently, it may indicate that you are writing overly specific selectors. Consider whether you can refactor your CSS to reduce the specificity of other selectors to avoid this problem. {% endAside %}
A matching specificity score sees the newest instance win
Let's stay with the button example for now and switch the CSS around to this:
.my-button {
background: blue;
}
[onclick] {
background: grey;
}
If you switch the rules in the source order, the button would then be blue.
[onclick] {
background: grey;
}
.my-button {
background: blue;
}
This is the only instance where newer CSS wins. To do so it must match the specificity of another selector that targets the same element.
Resources
- CSS SpeciFISHity
- Specificity Calculator
- MDN Specificity
- Specifics on CSS Specificity
- Another Specificity Calculator
The cascade
CSS stands for Cascading Stylesheets. The cascade is the algorithm for solving conflicts where multiple CSS rules apply to an HTML element. It's the reason that the text of the button styled with the following CSS will be blue.
button {
color: red;
}
button {
color: blue;
}
Understanding the cascade algorithm helps you understand how the browser resolves conflicts like this. The cascade algorithm is split into 4 distinct stages.
- Position and order of appearance: the order of which your CSS rules appear
- Specificity: an algorithm which determines which CSS selector has the strongest match
- Origin: the order of when CSS appears and where it comes from, whether that is a browser style, CSS from a browser extension, or your authored CSS
- Importance: some CSS rules are weighted more heavily than others,
especially with the
!importantrule type
Position and order of appearance
The order in which your CSS rules appear and how they appear is taken into consideration by the cascade while it calculates conflict resolution.
The demo right at the start of this lesson is the most straightforward example of position. There are two rules that have selectors of identical specificity, so the last one to be declared won.
Styles can come from various sources on an HTML page,
such as a <link> tag,
an embedded <style> tag,
and inline CSS as defined in an element's style attribute.
If you have a <link> that includes CSS at the top of your HTML page,
then another <link> that includes CSS at the bottom of your page: the bottom <link> will have the most specificity.
The same thing happens with embedded <style> elements, too.
They get more specific, the further down the page they are.
<link /> element.
A CSS rule that sets it to be dark is in a second linked stylesheet
and is applied because of its later position.This ordering also applies to embedded <style> elements.
If they are declared before a <link>,
the linked stylesheet's CSS will have the most specificity.
<style> element is declared in the <head>,
while the <link /> element is declared in the <body>.
This means it gets more specificity than the <style> elementAn inline style attribute with CSS declared in it will override all other CSS,
regardless of its position, unless a declaration has !important defined.
Position also applies in the order of your CSS rule.
In this example, the element will have a purple background because background: purple was declared last.
Because the green background was declared before the purple background, it is now ignored by the browser.
.my-element {
background: green;
background: purple;
}
Being able to specify two values for the same property
can be a simple way to create fallbacks for browsers that do not support a particular value.
In this next example, font-size is declared twice.
If clamp() is supported in the browser,
then the previous font-size declaration will be discarded.
If clamp() isn't supported by the browser,
the initial declaration will be honored, and the font-size will be 1.5rem
.my-element {
font-size: 1.5rem;
font-size: clamp(1.5rem, 1rem + 3vw, 2rem);
}
{% Aside %} This approach of declaring the same property twice works because browsers ignore values they don't understand. Unlike some other programming languages, CSS will not throw an error or break your program when it detects a line it cannot parse—the value it cannot parse is invalid and therefore ignored. The browser then continues to process the rest of the CSS without breaking stuff it already understands. {% endAside %}
{% Assessment 'position' %}
Specificity
Specificity is an algorithm which determines which CSS selector is the most specific, using a weighting or scoring system to make those calculations. By making a rule more specific, you can cause it to be applied even if some other CSS that matches the selector appears later in the CSS.
In the next lesson you can learn the details of how specificity is calculated, however keeping a few things in mind will help you avoid too many specificity issues.
CSS targeting a class on an element will make that rule more specific,
and therefore seen as more important to be applied,
than CSS targeting the element alone.
This means that with the following CSS,
the h1 will be colored red even though both rules match and the rule for the h1 selector comes later in the stylesheet.
<h1 class="my-element">Heading</h1>
.my-element {
color: red;
}
h1 {
color: blue;
}
An id makes the CSS even more specific,
so styles applied to an ID will override those applied many other ways.
This is one reason why it is generally not a good idea to attach styles to an id.
It can make it difficult to overwrite that style with something else.
Specificity is cumulative
As you can find out in the next lesson,
each type of selector is awarded points which indicate how specific it is,
the points for all of the selectors you have used to target an element are added together.
This means that if you target an element with a selector list such as
a.my-class.another-class[href]:hover you get something quite hard to overwrite with other CSS.
For this reason, and to help make your CSS more reusable,
it's a good idea to keep your selectors as simple as possible.
Use specificity as a tool to get at elements when you need to,
but always consider refactoring long, specific selector lists, if you can.
Origin
The CSS that you write isn't the only CSS applied to a page. The cascade takes into account the origin of the CSS. This origin includes the browser's internal stylesheet, styles added by browser extensions or the operating system, and your authored CSS. The order of specificity of these origins, from least specific, to most specific is as follows:
- User agent base styles. These are the styles that your browser applies to HTML elements by default.
- Local user styles. These can come from the operating system level, such as a base font size, or a preference of reduced motion. They can also come from browser extensions, such as a browser extension that allows a user to write their own custom CSS for a webpage.
- Authored CSS. The CSS that you author.
- Authored
!important. Any!importantthat you add to your authored declarations. - Local user styles
!important. Any!importantthat come from the operating system level, or browser extension level CSS. - User agent
!important. Any!importantthat are defined in the default CSS, provided by the browser.
If you have an !important rule type in the CSS you have authored
and the user has an !important rule type in their custom CSS, whose CSS wins?
{% Assessment 'origin' %}
Importance
Not all CSS rules are calculated the same as each other, or given the same specificity as each other.
The order of importance, from least important, to most important is as follows:
- normal rule type, such as
font-size,backgroundorcolor animationrule type!importantrule type (following the same order as origin)transitionrule type
Active animation and transition rule types have higher importance than normal rules.
In the case of transitions higher importance than !important rule types.
This is because when an animation or transition becomes active,
its expected behaviour is to change visual state.
Using DevTools to find out why some CSS is not applying
Browser DevTools will typically show all CSS that could match an element, with those which are not being used crossed out.
If the CSS you expected to apply doesn't appear at all, then it didn't match the element. In that case you need to look elsewhere, perhaps for a typo in a class or element name or some invalid CSS.
{% Assessment 'conclusion' %}
Resources
Transitions
When interacting with a website, you might notice that many elements have state. For example, dropdowns can be in opened or closed states. Buttons might change color when focused or hovered. Modals appear and disappear.
By default, CSS switches the style of these states instantly.
Using CSS transitions, we can interpolate between the initial state and the target state of the element. The transition between the two enhances the user experience by providing visual direction, support, and hints about the cause and effect of the interaction.
{% Aside 'key-term' %} Interpolation is the process of creating "in-between" steps that smoothly transition from one state to another. {% endAside %}
Transition properties
{% BrowserCompat 'css.properties.transition-property' %}
To use transitions in CSS, you can use the various transition properties or the transition shorthand property.
transition-property
The transition-property property specifies which style(s) to transition.
.my-element {
transition-property: background-color;
}
The transition-property accepts one or more CSS property names in a comma-separated list.
Optionally, you may use transition-property: all to indicate that every property should transition.
transition-duration
The transition-duration property is used to define the length of time that a transition will take to complete.
transition-duration accepts time units, either in seconds (s) or milliseconds (ms) and defaults to 0s.
transition-timing-function
Use the transition-timing-function property to vary the speed of a CSS transition over the course of the transition-duration.
By default, CSS will transition your elements at a constant speed (transition-timing-function: linear). Linear transitions can end up looking somewhat artificial, though: in real life, objects have weight and can't stop and start instantly. Easing into or out of a transition can make your transitions more lively and natural.
Our module on CSS Animation has a good overview of timing functions.
You can use DevTools to experiment with different timing functions in real-time.
{% Img src="image/eiKy1JcjHqPp3gaedjAQWjPJ8YK2/dRwKg0RIsy5wWVzkUFUA.png", alt="Chrome DevTools visual transition timing editor.", width="800", height="418" %}
transition-delay
Use the transition-delay property to specify the time at which a transition will start. If transition-duration is not specified, transitions will start instantly because the default value is 0s. This property accepts a time unit, for example seconds (s) or milliseconds (ms).
This property is useful for staggering transitions, achieved by setting a longer transition-delay for each subsequent element in a group.
transition-delay is also useful for debugging. Setting the delay to a negative value can start a transition further into the timeline.
shorthand: transition
Like most CSS properties, there is a shorthand version. transition combines transition-property, transition-duration, transition-timing-function, and transition-delay.
.longhand {
transition-property: transform;
transition-duration: 300ms;
transition-timing-function: ease-in-out;
transition-delay: 0s;
}
.shorthand {
transition: transform 300ms ease-in-out 0s;
}
What can and can't transition?
When writing CSS, you can specify which properties should have animated transitions. See this MDN list of animatable CSS properties.
In general, it's only possible to transition elements that can have a "middle state" between their start and final states. For example, it's impossible to add transitions for font-family, because it's unclear what the "middle state" between serif and monospace should look like. On the other hand, it is possible to add transitions for font-size because its unit is a length that can be interpolated between.
{% Img src="image/eiKy1JcjHqPp3gaedjAQWjPJ8YK2/1Lr1iC56nyUyYWWv6qF2.jpg", alt="Diagram of shapes transitioning smoothly from one state to another, and two lines of text in different fonts that cannot be transitioned smoothly.", width="800", height="600" %}
Here are some common properties you can transition.
Transform
{% BrowserCompat 'css.properties.transform' %}
The transform CSS property is commonly transitioned because it is a GPU-accelerated property that results in smoother animation that also consumes less battery. This property lets you arbitrarily scale, rotate, translate, or skew an element.
Check out the section on transforms in our Functions module.
Color
Before, during, and after interaction, color can be a great indicator of state. For example, a button might change color if it's being hovered. This color change can provide feedback to the user that the button is clickable.
The color, background-color, and border-color properties are just a few places where color can
be transitioned upon interaction.
{% Aside %} Color transitions do not usually need to be behind a reduced motion preference. Use your best judgment. {% endAside %}
Check out our module on color.
Shadows
Shadows are often transitioned to indicate elevation change, like from user focus.
Check out our module on shadows.
Filters
filter is a powerful CSS property that lets you add graphic effects on the fly. Transitioning between different filter states can create some pretty impressive results.
Check out our module on filters.
Transition triggers
Your CSS must include a change of state and an event that triggers that state change for CSS transitions to activate. A typical example of such a trigger is the :hover pseudo-class. This pseudo-class matches when the user hovers over an element with their cursor.
Below is a list of some pseudo-classes and events that can trigger state changes in your elements.
:hover: matches if the cursor is over the element.:focus: matches if the element is focused.:focus-within: matches if the element or any of its descendants are focused.:target: matches when the current URL's fragment matches the element's id.:active: matches when the element is being activated (typically when the mouse is pressed over it).classchange from JavaScript: when an element's CSSclasschanges via JavaScript, CSS will transition eligible properties that have changed.
Different transitions for enter or exit
By setting different transition properties on hover/focus, it's possible to create some interesting effects.
.my-element {
background: red;
/* This transition is applied on the "exit" transition */
transition: background 2000ms ease-in;
}
.my-element:hover {
background: blue;
/* This transition is applied on the "enter" transition */
transition: background 150ms ease;
}
Accessibility considerations
CSS transitions are not for everyone. For some people, transitions and animations can cause motion sickness or discomfort. Thankfully, CSS has a media feature called prefers-reduced-motion that detects if a user has indicated a preference for less motion from their device.
/*
If the user has expressed their preference for
reduced motion, then don't use transitions.
*/
@media (prefers-reduced-motion: reduce) {
.my-element {
transition: none;
}
}
/*
If the browser understands the media query and the user
explicitly hasn't set a preference, then use transitions.
*/
@media (prefers-reduced-motion: no-preference) {
.my-element {
transition: transform 250ms ease;
}
}
Check out our blog post prefers-reduced-motion: Sometimes less movement is more for more information on this media feature.
{% Aside %} In the Learn Accessibility module on animation and motion discover how to add delight to your site without causing problems for some users. {% endAside %}
Performance considerations
When working with CSS transitions, you may encounter performance issues if you add transitions for certain CSS properties. For example, when properties such as width or height change, they push content around on the rest of the page. This forces CSS to calculate new positions for every affected element for each frame of the transition. When possible, we recommend using properties like transform and opacity instead.
Check out our guide on high-performance CSS animations for a deep-dive on this topic.
{% Assessment 'transitions' %}
Text and typography
Text is one of the core building blocks of the web.
When making a website, you don’t necessarily need to style your text; HTML actually has some pretty reasonable default styling.
However, text will likely make up the majority of your website, so it’s worthwhile to add some styling to spruce it up. By changing a few basic properties, you can significantly improve the reading experience for your users!
In this module, we’ll first look at some fundamental CSS font properties like font-family, font-style, font-weight, and font-size. Then, we’ll dive into properties that affect paragraphs of text, such as text-indent and word-spacing. The module finishes with some more advanced topics such as variable fonts and pseudo-elements.
Change the typeface
{% BrowserCompat 'css.properties.font-family' %}
Use font-family to change the typeface of your text.
The font-family property accepts a comma-separated list of strings, either referring to specific or generic font families. Specific font families are quoted strings, such as “Helvetica”, “EB Garamond”, or “Times New Roman”. Generic font families are keywords such as serif, sans-serif, and monospace (find the full list of options on MDN). The browser will display the first available typeface from the provided list.
{% Aside %}
When the browser chooses which font to display from your font-family declaration, it doesn’t stop at the first available font in the list. Instead, it selects fonts one character at a time. If a particular character isn’t available in the first font in the list, it moves on to the next, and so on.
{% endAside %}
When using font-family, you should specify at least one generic font family in case the user’s browser doesn’t have your preferred fonts. Generally, the fallback generic font family should be similar to your preferred fonts: if using font-family: "Helvetica" (a sans-serif font family), your fallback should be sans-serif to match.
Use italic and oblique fonts
{% BrowserCompat 'css.properties.font-style' %}
Use font-style to set whether text should be italic or not. font-style accepts one of the following keywords: normal, italic, and oblique.
{% Aside %}
Q: What’s the difference between italic and oblique?
A: In fonts that support it, font-style: italic is typically a cursive version of the regular typeface. font-style: oblique displays a slanted version of regular typeface.
{% endAside %}
Make text bold
{% BrowserCompat 'css.properties.font-weight' %}
Use font-weight to set the “boldness” of text. This property accepts keyword values (normal, bold), relative keyword values (lighter, bolder), and numeric values (100 to 900).
The keywords normal and bold are equivalent to the numeric values 400 and 700, respectively.
The keywords lighter and bolder are calculated relative to the parent element. See MDN’s Meaning of Relative Weights for a handy chart showing how this value is determined.
{% Aside %}
Most fonts, especially the "web-safe" ones, only support the weights 400 (normal) and 700 (bold). When importing fonts using @font-face or @import, you can choose specific weights you want to pull in. Still, non-variable fonts only support numeric values for font-weight in the 100s, e.g. 100, 200, 300, etc. If you want to use font-weight: 321 (for example), you’ll have to use a Variable Font.
{% endAside %}
Change the size of text
{% BrowserCompat 'css.properties.font-size' %}
Use font-size to control the size of your text elements. This property accepts length values, percentages, and a handful of keyword values.
In addition to length and percentage values, font-size accepts some absolute keyword values (xx-small, x-small, small, medium, large, x-large, xx-large) and a couple of relative keyword values (smaller, larger). The relative values are relative to the parent element’s font-size.
{% Aside %}
Q: What’s the difference between em and rem?
A: In CSS, em represents the font-size inherited from the element’s parent. For example, font-size: 2em is equivalent to the parent’s font-size multiplied by two. rem is similar, but instead represents the font-size inherited from the root element, i.e. <html>.
{% endAside %}
Change the space between lines
{% BrowserCompat 'css.properties.line-height' %}
Use line-height to specify the height of each line in an element. This property accepts either a number, length, percentage, or the keyword normal. Generally, it’s recommended to use a number instead of a length or percentage to avoid issues with inheritance.
Change the space between characters
{% BrowserCompat 'css.properties.letter-spacing' %}
Use letter-spacing to control the amount of horizontal space between characters in your text. This property accepts length values such as em, px, and rem.
Note that the specified value increases the amount of natural space between characters. In the demo below, try selecting an individual letter to see the size of its letterbox and how it changes with letter-spacing.
Change the space between words
{% BrowserCompat 'css.properties.word-spacing' %}
Use word-spacing to increase or decrease the length of space between each word in your text. This property accepts length values such as em, px, and rem. Note that the length you specify is for extra space in addition to the normal spacing. This means that word-spacing: 0 is equivalent to word-spacing: normal.
font shorthand
You can use the shorthand font property to set many font-related properties at once. The list of possible properties are font-family, font-size, font-stretch, font-style, font-variant, font-weight, and line-height.
Check out MDN’s font article for the specifics of how to order these properties.
Change the case of text
{% BrowserCompat 'css.properties.text-transform' %}
Use text-transform to modify the capitalization of your text without needing to change the underlying HTML. This property accepts the following keyword values: uppercase, lowercase, and capitalize.
Add underlines, overlines, and through-lines to text
{% BrowserCompat 'css.properties.text-decoration' %}
Use text-decoration to add lines to your text. Underlines are most commonly used, but it’s possible to add lines above your text or right through it!
The text-decoration property is shorthand for the more specific properties detailed below.
The text-decoration-line property accepts the keywords underline, overline, and line-through. You can also specify multiple keywords for multiple lines.
The text-decoration-color property sets the color of all decorations from text-decoration-line.
The text-decoration-style property accepts the keywords solid, double, dotted, dashed, and wavy.
The text-decoration-thickness property accepts any length values and sets the stroke width of all decorations from text-decoration-line.
The text-decoration property is a shorthand for all the above properties.
{% Aside %}
Use text-underline-position to offset the underline of a text-decoration: underline by the specified amount. This property doesn’t work for overline or line-through.
{% endAside %}
Add an indent to your text
{% BrowserCompat 'css.properties.text-indent' %}
Use text-indent to add an indent to your blocks of text. This property takes either a length (for example, 10px, 2em) or a percentage of the containing block’s width.
Deal with overflowing or hidden content
{% BrowserCompat 'css.properties.text-overflow' %}
Use text-overflow to specify how hidden content is represented. There are two options: clip (the default), which truncates the text at the point of overflow; and ellipsis, which displays an ellipsis (…) at the point of overflow.
Control white-space
{% BrowserCompat 'css.properties.white-space' %}
The white-space property is used to specify how whitespace in an element should be handled. For more details, check out the white-space article on MDN.
white-space: pre can be useful for rendering ASCII art or carefully indented code blocks.
Control how words break
{% BrowserCompat 'css.properties.word-break' %}
Use word-break to change how words should be “broken” when they would overflow the line. By default, the browser will not split words. Using the keyword value break-all for word-break will instruct the browser to break words at individual characters if necessary.
Change text alignment
{% BrowserCompat 'css.properties.text-align' %}
Use text-align to specify the horizontal alignment of text in a block or table-cell element. This property accepts the keyword values left, right, start, end, center, justify, and match-parent.
The values left and right align the text to the left and right sides of the block, respectively.
Use start and end to represent the location of the start and end of a line of text in the current writing mode. Therefore, start maps to left in English, and to right in Arabic script which is written right to left (RTL). They're logical alignments, learn more in our logical properties module.
Use center to align the text to the center of the block.
The value of justify organizes the text and changes word spacings automatically so that the text lines up with both the left and right edges of the block.
Change the direction of text
{% BrowserCompat 'css.properties.direction' %}
Use direction to set the direction of your text, either ltr (left to right, the default) or rtl (right to left). Some languages like Arabic, Hebrew, or Persian are written right to left, so direction: rtl should be used. For English and all other left-to-right languages, use direction: ltr.
{% Aside 'caution' %}
Generally, you should favor using the HTML attribute dir instead of direction. Check out this StackOverflow discussion for more details.
{% endAside %}
Change the flow of text
{% BrowserCompat 'css.properties.writing-mode' %}
Use writing-mode to change the way text flows and is arranged. The default is horizontal-tb, but you can also set writing-mode to vertical-lr or vertical-rl for text that you want to flow horizontally.
Change the orientation of text
{% BrowserCompat 'css.properties.text-orientation' %}
Use text-orientation to specify the orientation of characters in your text. The valid values for this property are mixed and upright. This property is only relevant when writing-mode is set to something other than horizontal-tb.
Add a shadow to text
{% BrowserCompat 'css.properties.text-shadow' %}
Use text-shadow to add a shadow to your text. This property expects three lengths (x-offset, y-offset, and blur-radius) and a color.
Check out the text-shadow section of our module on Shadows to learn more.
Variable fonts
Typically, “normal” fonts require importing different files for different versions of the typeface, e.g. bold, italic, or condensed. Variable fonts are fonts that can contain many different variants of a typeface in one file.
Check out our article on Variable Fonts for more details.
Pseudo-elements
{% Aside 'key-term' %} A pseudo-element is a part of an element that you can target via CSS keywords without having to add more HTML. Check out our module on pseudo-elements for a deep-dive into this subject! {% endAside %}
::first-letter and ::first-line pseudo-elements
{% BrowserCompat 'css.selectors.first-letter' %}
The ::first-letter and ::first-line pseudo-elements target a text element’s first letter and first line respectively.
::selection pseudo-element
{% BrowserCompat 'css.selectors.selection' %}
Use the ::selection pseudo-element to change the appearance of user-selected text.
When using this pseudo-element, only certain CSS properties can be used: color, background-color, text-decoration, text-shadow, stroke-color, fill-color, stroke-width.
font-variant
{% BrowserCompat 'css.properties.font-variant' %}
The font-variant property is a shorthand for a number of CSS properties that let you choose font variants like small-caps and slashed-zero. The CSS properties this shorthand includes are font-variant-alternates, font-variant-caps, font-variant-east-asian, font-variant-ligatures, and font-variant-numeric. Check out the links on each property for more details about its usage.
{% Assessment 'typography' %}
Resources
- Font best practices discusses importing fonts, rendering fonts, and other best practices for using fonts on the web.
- MDN Fundamental text and font styling.
Z-index and stacking contexts
Say you've got a couple of elements that are absolutely positioned, and are supposed to be positioned on top of each other. You might write a bit of a HTML like this:
<div class="stacked-items">
<div class="item-1">Item 1</div>
<div class="item-2">Item 2</div>
</div>
But which one sits on top of the other, by default? To know which item would do that, you need to understand z-index and stacking contexts.
Z-index
The z-index
property explicitly sets a layer order for HTML based on the 3D space of the browser—the Z axis.
This is the axis which shows which layers are closer to and further from you.
The vertical axis on the web is the Y axis and the horizontal axis is the X axis.
{% Img src="image/VbAJIREinuYvovrBzzvEyZOpw5w1/PDglCLEK0OJY5LIHwpkI.svg", alt="Each axis surrounding the element", width="760", height="467" %}
The z-index property accepts a numerical value which can be a positive or negative number.
Elements will appear above another element if they have a higher z-index value.
If no z-index is set on your elements
then the default behaviour is that document source order dictates the Z axis.
This means that elements further down the document sit on top of elements that appear before them.
In normal flow,
if you set a specific value for z-index and it isn't working,
you need to set the element's position value to anything other than static.
This is a common place where people struggle with z-index.
This isn't the case if you are in a flexbox or grid context, though,
because you can modify the z-index of flex or grid items without adding position: relative.
Negative z-index
To set an element behind another element,
add a negative value for z-index.
.my-element {
background: rgb(232 240 254 / 0.4);
}
.my-element .child {
position: relative;
z-index: -1;
}
As long as .my-element has the initial value for z-index of auto,
the .child element will sit behind it.
Add the following CSS to .my-element,
and the .child element will not sit behind it.
.my-element {
position: relative;
z-index: 0;
background: rgb(232 240 254 / 0.4);
}
Because .my-element now has a position value that's not static
and a z-index value that's not auto,
it has created a new stacking context.
This means that even if you set .child to have a z-index of -999,
it would still not sit behind .my-parent.
Stacking context
A stacking context is a group of elements that have a common parent and move up and down the z axis together.
In this example,
the first parent element has a z-index of 1,
so creates a new stacking context.
Its child element has a z-index of 999.
Next to this parent, there is another parent element with one child.
The parent has a z-index of 2 and the child element also has a z-index of 2.
Because both parents create a stacking context,
the z-index of all children is based on that of their parent.
The z-index of elements inside of a stacking context
are always relative to the parent's current order in its own stacking context.
{% Aside %}
The <html> element is a stacking context itself and nothing can ever go behind it.
You can put stuff behind the <body> until you create a stacking context with it.
{% endAside %}
Creating a stacking context
You don't need to apply z-index and position to create a new
stacking context.
You can create a new stacking context by adding a value for properties which create a new composite layer
such as opacity, will-change and transform.
You can
see a full list of properties here.
To explain what a composite layer is, imagine a web page is a canvas. A browser takes your HTML and CSS and uses these to work out how big to make the canvas. It then paints the page on this canvas. If an element was to change—say, it changes position—the browser then has to go back and re-work out what to paint.
To help with performance,
the browser creates new composite layers which are layered on top of the canvas.
These are a bit like post-it notes:
moving one around and changing it doesn't have a huge impact on the overall canvas.
A new composite layer is created for elements with opacity,
transform and will-change because these are very likely to change,
so the browser makes sure that change is performant as possible by using the GPU to apply style adjustments.
{% Aside %}
You can also create a stacking context by adding a filter and setting backface-visibility: hidden.
{% endAside %}
Resources
{% Assessment 'z-index' %}
Learn Responsive Design
This course takes you on a journey through the many facets of modern responsive web design. The first few modules will ease you in with a history of where responsive design came from and a look at the fundamentals of responsive layouts. From there, you’ll learn about responsive images, typography, accessibility and more.
Along the way you’ll find out how to make websites responsive to user preferences and device capabilities. By the end, you’ll also have an understanding of what the future might hold for responsive design.
Each module has demos and self-assessments for you to test your knowledge.
This course is created for beginner and intermediate designers and developers. A basic understanding of HTML and CSS should be enough. You don’t need to know any JavaScript. If you’re completely new to making websites, there's an introduction to HTML and another course to help you learn CSS.
Here's what you'll learn:
{% include 'partials/course-index.njk' %}
So, are you ready to learn responsive design?
{% Aside %} This course was written by Jeremy Keith, with input and review from Rachel Andrew, Una Kravets, Adam Argyle, and Camden Bickel. {% endAside %}
Accessibility
Allowing your pages to respond to different screen sizes is just one way of making sure your website is accessible to as many people as possible. Consider some of these other factors that you should keep in mind.
Color vision deficiency
Different people perceive color differently. People with protanopia don't perceive red as a distinct color. With deuteranopia, green is missing. For people with tritanopia, it's blue.
Some tools can give you a general idea of how your color schemes appear to people with different kinds of color vision.
Firefox's Accessibility tab includes a dropdown labeled Simulate with a list of options.
In Chrome DevTools, the rendering tab allows you to emulate vision deficiencies.
Those are browser-specific tools. It's also possible to emulate different vision types at the operating system level.
On the Mac, go to:
- System Preferences
- Accessibility
- Display
- Color Filters
- Enable Color Filters
Select one of the options.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/eKvwhSc6RHvS9KFG7pU6.png", alt="Color filter options in system preferences.", width="780", height="602" %}
In general it's not a good idea to rely purely on color to differentiate between different elements. For example, you can—and should—make your links a different color to the surrounding text. But you should also apply some other styling indicator like underlining the links or making them bold.
{% Compare 'worse' %}
a {
color: red;
}
{% endCompare %}
{% Compare 'better' %}
a {
color: red;
font-weight: bold;
}
{% endCompare %}
Color contrast
Some color combinations can cause trouble. If there isn't enough contrast between the foreground color and background color, text becomes difficult to read. Poor color contrast is one of the most common accessibility issues on the web, but fortunately, it's one that you can catch early in the design process.
Here are some tools you can use to test the contrast ratio of your text and background colors:
- tota11y is a bookmarklet you can add to your browser's toolbar.
- VisBug is a browser extension available for all major desktop browsers.
- Firefox's Accessibility Inspector can check for issues with visual contrast.
- You can also discover and fix low-contrast text with Chrome DevTools.
- In Microsoft's Edge browser, you can test text-color contrast using the color picker.
It's a good idea to always declare color and background-color together in your CSS. Don't assume that the background color will be the browser default. People can and do change the colors used by their browser.
{% Compare 'worse' %}
body {
color: black;
}
{% endCompare %}
{% Compare 'better' %}
body {
color: black;
background-color: white;
}
{% endCompare %}
High contrast
Some people set their operating systems to use a high-contrast mode. You can try this on your operating system.
On the Mac, go to:
- System Preferences
- Accessibility
- Display
Select the option to increase contrast.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/WRYeXsiJVP4nP1b8xAOr.png", alt="Increase contrast in system preferences.", width="780", height="602" %}
There's a media feature to detect if someone has enabled high contrast mode. The prefers-contrast media feature can be queried for three values: no-preference, less, and more. You can use this information to adjust your site's color palette.
{% BrowserCompat 'css.at-rules.media.prefers-contrast' %}
{% Codepen { user: 'web-dot-dev', id: 'abLBaya', height: 500, theme: 'dark', tab: 'css,result' } %}
{% Aside %}
CSS custom properties are a great way of grouping color declarations. Use the prefers-contrast media feature to update your custom property values.
{% endAside %}
People can also set a preference in their operating system to use inverted colors.
On the Mac, go to:
- System Preferences
- Accessibility
- Display
Select the option to invert colors.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/pDi1fFRwChp7xpRbfqRC.png", alt="Invert colors in system preferences.", width="780", height="602" %}
Make sure your website still makes sense for someone browsing with inverted colors. Watch out for box shadows—these may need adjusting when colors are inverted.
Font size
Color isn't the only thing that people can adjust in their browser, they can also adjust the default font size. As their vision declines, they might adjust the default font size in their browsers or operating systems, bumping up the numbers with the passing of the years.
You can respond to these settings by using relative font sizes. Avoid using units like px. Use relative units like rem or ch instead.
Try changing the default text size setting in your browser. You can do that in your browser preferences. Or you can do it while you're visiting a web page by zooming in. Does your website still work if the default font size is increased by 200%? How about 400%?
Someone visiting your website on a desktop computer with their font size bumped up to 400% should get the same layout as someone visiting your site on a small-screen device.
Keyboard navigation
Not everyone uses a mouse or a trackpad to navigate web pages. A keyboard is another way of getting around a page, with the tab key being particularly useful. Users can quickly move from one link or form field to the next.
Links styled with the :hover and :focus pseudo-classes, will display those styles regardless of whether someone is using a mouse, a trackpad, or a keyboard. Use the :focus-visible pseudo-class to style your links for just keyboard navigation. You can make those styles extra noticeable.
a:focus,
a:hover {
outline: 1px dotted;
}
a:focus-visible {
outline: 3px solid;
}
{% Codepen { user: 'web-dot-dev', id: 'wvroErW', height: 400, theme: 'dark', tab: 'result' } %}
As the user tabs from link to link or form field to form field, those elements will be focused in the order they appear in the document structure. This should also match the visual order.
Be careful with the CSS order property. You can use this in combination with flexbox and grid to place elements in a different visual order to their order in the HTML. That's a powerful feature but it could confuse people navigating with a keyboard.
{% Codepen { user: 'web-dot-dev', id: 'XWeNPeY', height: 400, theme: 'dark', tab: 'result' } %}
Test your web pages by using the tab key on your keyboard to make sure that the tabbing order makes sense.
In the Accessibility panel of the Firefox browser's developer tools there's an option to Show Tabbing Order. Enabling this will overlay numbers on each focusable element.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/4JFy0QWWiHW0x7X4aCxq.png", alt="Visualizing tab order using Firefox's Accessibility Tab.", width="800", height="391" %}
{% Aside %} Learn more about the problems caused by content reordering. {% endAside %}
Reduced motion
Animation and motion are wonderful ways to bring web designs to life. But for some people these movements can be very disorienting and even cause nausea.
There's a feature query that communicates whether the user would prefer less motion. It's called prefers-reduced-motion. Include it wherever you are using CSS transitions or animations.
{% BrowserCompat 'css.at-rules.media.prefers-reduced-motion' %}
a:hover {
transform: scale(150%);
}
@media (prefers-reduced-motion: no-preference) {
a {
transition-duration: 0.4s;
transition-property: transform;
}
}
The prefers-reduced-motion media query is specifically for movement on the screen. If you are using transitions on an element's color that shouldn't be affected by prefers-reduced-motion. It's also ok to transition opacity and cross-fade. Reduced motion doesn't have to mean no animation.
Voice
People experience the web differently. Not everyone is seeing your website on a screen. Assistive technologies such as screen readers convert the information output to a screen into spoken words.
Screen readers work with all kinds of applications including web browsers. In order for a web browser to communicate usefully with a screen reader, there needs to be useful semantic information in the web page currently being accessed.
Previously, you learned how icon-only buttons need to include an attribute to specify the button's purpose to non-sighted users. This is just one example of the importance of strong foundational HTML.
Headings
Use headings like <h1>, <h2>, <h3>, etc. sensibly. Screen readers use these headings to generate an outline of your document which can be navigated with keyboard shortcuts.
{% Compare 'worse' %}
<div class="heading-main">Welcome to my page</div>
<div class="heading-secondary">About me</div>
<div class="heading-tertiary">My childhood</div>
<div class="heading-secondary">About this website</div>
<div class="heading-tertiary">How this site was built</div>
{% endCompare %}
{% Compare 'better' %}
<h1>Welcome to my page</h1>
<h2>About me</h2>
<h3>My childhood</h3>
<h2>About this website</h2>
<h3>How this site was built</h3>
{% endCompare %}
Structure
Use landmark elements like <main>, <nav>, <aside>, <header>, and <footer> to structure your page's contents. Screen-reader users can then jump straight to these landmarks.
{% Compare 'worse' %}
<div class="header">...</div>
<div class="navigation">...</div>
<div class="maincontent">...</div>
<div class="sidebar">...</div>
<div class="footer">...</div>
{% endCompare %}
{% Compare 'better' %}
<header>...</header>
<nav>...</nav>
<main>...</main>
<aside>...</aside>
<footer>...</footer>
{% endCompare %}
Forms
Make sure that every form field has an associated <label> element. You can associate a label with a form field using the for attribute on the <label> element and the corresponding id attribute on the form field.
{% Compare 'worse' %}
<span class="formlabel">Your name</span>
<input type="text">
{% endCompare %}
{% Compare 'better' %}
<label for="name">Your name</label>
<input id="name" type="text">
{% endCompare %}
{% Aside %} For more information in accessibility and forms, see the Accessibility module in the Learn Forms course. {% endAside %}
Images
Always provide a text description of images using the alt attribute.
{% Compare 'worse' %}
<img src="dog.jpg">
{% endCompare %}
{% Compare 'better' %}
<img src="dog.jpg" alt="A golden retriever sitting on the grass looking happy.">
{% endCompare %}
If the image is purely presentational, you should still include the alt attribute but you can give it an empty value.
{% Compare 'worse' %}
<img src="texture.png">
{% endCompare %}
{% Compare 'better' %}
<img src="texture.png" alt="">
{% endCompare %}
Jake Archibald published an article on writing great alt text.
Links
Try to provide descriptive text within links. Avoid using phrases like "click here" or "more."
{% Compare 'worse' %}
<p>To find out more about our latest offers, <a href="offers.html">click here</a>.</p>
{% endCompare %}
{% Compare 'better' %}
<p>Find out more about <a href="offers.html"> our latest offers</a>.</p>
{% endCompare %}
ARIA
Using sensible semantic HTML will make your web pages more accessible to assistive technologies like screen readers and also to other audio outputs like voice assistants.
Some interface widgets that don't have a corresponding HTML element: carousels, tabs, accordions, and so on. Those need to be built from scratch with a combination of HTML, CSS, JavaScript, and ARIA.
ARIA stands for Accessible Rich Internet Applications. Its vocabulary allows you to provide semantic information when there isn't a suitable HTML element available.
If you need to create interface elements that aren't yet available as HTML elements, familiarize yourself with ARIA.
The more bespoke functionality you add with JavaScript, the more you'll need to understand ARIA. If you stick with native HTML elements, you may not need any ARIA.
If it's at all possible, test with real users of screen readers. Not only will this give you a better understanding of how they navigate the web, it will also take the guesswork out of designing with accessibility in mind.
Testing with actual people is a great way of exposing any assumptions you might be making. In the next module, you'll learn about the different ways people interact with your websites—another area where it's all too easy to make assumptions.
{% Assessment 'accessibility' %}
Conclusion and next steps
Congratulations! You've made it to the end! We hope that you have enjoyed this overview of responsive design. For more, you can check out the following additional resources:
Additional resources
- LearnCSS
- CSS Documentation on MDN
- Listen to the CSS Podcast
- CSS articles on web.dev
- CSS-Tricks
- Layout Land - Youtube
Brought to you by
{% include 'partials/authors-stack.njk' %}
Icons
Most images are part of your content, but icons are part of your user interface. They should scale and adapt in the same way that the text of your UI scales and adapts.
Scalable Vector Graphics
When it comes to photographic imagery, there are lots of choices for the image format: JPG, WebP, and AVIF. For non-photographic imagery, you have a choice between the Portable Network Graphics (PNG) format and the Scalable Vector Graphics (SVG) format.
Both PNGs and SVGs are good at dealing with areas of flat color, and they both allow your images to have transparent backgrounds.
If you use a PNG you'll probably need to make multiple versions of your image in different sizes and use the srcset attribute on your img element to make the image responsive. If you use an SVG, it's responsive by default.
PNGs (and JPGs, WebP, and AVIF) are bitmap images. Bitmap images store information as pixels. In an SVG, information is stored as drawing instructions. When the browser reads the SVG file the instructions are converted into pixels. Best of all, these instructions are relative. Regardless of the dimensions you use to describe lines and shapes, everything renders at just the right crispness. Instead of creating multiple SVGs of different sizes you can make one SVG that will work at all sizes. There's no need to use the srcset attribute.
<img src="image.svg" alt="A description of the image." width="25" height="25">
<img src="image.svg" alt="A description of the image." width="250" height="250">
{% Codepen { user: 'web-dot-dev', id: 'YzrKoWY', height: 360, theme: 'dark', tab: 'html,result' } %}
XML is used to write the instructions in an SVG file. This is a markup language, like HTML.
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-21 -21 42 42" width="100" height="100">
<title>Smiling face</title>
<circle r="20" fill="yellow" stroke="black"/>
<ellipse rx="2.5" ry="4" cx="-6" cy="-7" fill="black"/>
<ellipse rx="2.5" ry="4" cx="6" cy="-7" fill="black"/>
<path stroke="black" d="M -12,5 A 13.5,13.5,0 0,0 12,5 A 13,13,0 0,1 -12,5"/>
</svg>
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/3F9ZFc3oOqhrPN6YU0Bd.svg", alt="Smiley face.", width="800", height="800" %}
You can even put SVG inside HTML.
<figure>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-21 -21 42 42" width="100" height="100">
<title>Smiling face</title>
<circle r="20" fill="yellow" stroke="black"/>
<ellipse rx="2.5" ry="4" cx="-6" cy="-7" fill="black"/>
<ellipse rx="2.5" ry="4" cx="6" cy="-7" fill="black"/>
<path stroke="black" d="M -12,5 A 13.5,13.5,0 0,0 12,5 A 13,13,0 0,1 -12,5"/>
</svg>
<figcaption>
A description of the image.
</figcaption>
</figure>
If you embed an SVG like that, that's one less request that the browser needs to make. There'll be no wait for the image to download because it arrives with the HTML …in the HTML! Also, as you'll soon find out, embedding SVGs like this gives you more control over styling them too.
Icons and text
Icon images often feature simple shapes on a transparent background. SVG is ideal for icons.
{% Aside %}
An empty alt attribute is not the same as a missing alt attribute. Always provide an alt attribute even if an image is presentational and the alt attribute has no content.
{% endAside %}
<button>
<img src="hamburger.svg" alt="" width="16" height="16">
Menu
</button>
{% Codepen { user: 'web-dot-dev', id: 'qBPWzam', height: 200, theme: 'dark', tab: 'html,result' } %}
Because CSS is for presentation, you could put the icon in your CSS as a background image.
<button class="menu">
Menu
</button>
.menu {
background-image: url(hamburger.svg);
background-position: 0.5em;
background-repeat: no-repeat;
background-size: 1em;
padding-inline-start: 2em;
}
{% Codepen { user: 'web-dot-dev', id: 'gOGYNwj', height: 200, theme: 'dark', tab: 'html,result' } %}
If you put the SVG inside your HTML, use the aria-hidden attribute to hide it from assistive technology.
<button class="menu">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 80" width="16" height="16">
<rect width="100" height="20" />
<rect y="30" width="100" height="20"/>
<rect y="60" width="100" height="20"/>
</svg>
Menu
</button>
{% Codepen { user: 'web-dot-dev', id: 'bGobPwz', height: 200, theme: 'dark', tab: 'html,result' } %}
{% Aside %}
If you use the same icon multiple times in one page, it would be inefficient to repeat the entire SVG markup each time. There's an element in SVG called use which allows you to “clone” part of an SVG, even from a different SVG element.
{% endAside %}
Standalone icons
Use text inside your buttons and links if you want their purpose to be clear. You can use an icon without any text but don't assume that everyone understands the meaning of a particular icon. When in doubt, test with real users.
If you decide to use an icon without any accompanying text, the icon is no longer presentational. A background image in CSS is not an appropriate way to display the icon. The icon needs to be given an accessible name in HTML.
If you use an img element, the icon gets its accessible name from the alt attribute.
{% Aside %}
Usually the alt attribute describes the contents of the image (“Three horizontal lines.”) but with standalone icons, the alt attribute describes the meaning of the image (“Menu”).
{% endAside %}
<button>
<img src="hamburger.svg" alt="Menu"" width="16" height="16">
</button>
{% Codepen { user: 'web-dot-dev', id: 'MWEgMbY', height: 200, theme: 'dark', tab: 'html,result' } %}
Another option is to put the accessible name on the link or button itself and declare that the image is presentational. Use the aria-label attribute to provide the accessible name.
<button aria-label="Menu">
<img src="hamburger.svg" alt="" width="16" height="16">
</button>
{% Codepen { user: 'web-dot-dev', id: 'VwMZJmr', height: 200, theme: 'dark', tab: 'html,result' } %}
If you put the SVG inside your HTML, use the aria-label attribute on the link or button to give it an accessible name and use the aria-hidden attribute on the icon.
<button aria-label="Menu">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 80" width="16" height="16">
<rect width="100" height="20" />
<rect y="30" width="100" height="20"/>
<rect y="60" width="100" height="20"/>
</svg>
</button>
{% Codepen { user: 'web-dot-dev', id: 'OJxLebB', height: 200, theme: 'dark', tab: 'html,result' } %}
Styling icons
If you embed your SVG icons directly in your HTML you can style parts of them just like any other element in your page. You can't do that if you use an img element to display your icons.
In the following example, the rect elements inside the SVG have a fill value of blue to match the styles for the button's text.
button {
color: blue;
}
button rect {
fill: blue;
}
You can apply hover and focus styles too.
button:hover,
button:focus {
color: red;
}
button:hover rect,
button:focus rect {
fill: red;
}
{% Codepen { user: 'web-dot-dev', id: 'rNGBEje', height: 200, theme: 'dark', tab: 'html,result' } %}
Resources
- If you need to style SVGs that are background images in your CSS, read Una's article on colorizing SVG backgrounds.
- Sara Soueidan has written about accessible icon buttons.
- Scott O'Hara has written about contextually marking up accessible images and SVGs.
- If you're using a graphic design tool to export SVGs, use SVGOMG to optimize the output.
Icons are an important part of your site's branding. Next you'll find out how to make other aspects of your branding responsive through the power of theming.
{% Assessment 'icons' %}
Interaction
Small screen devices like mobile phones often have touchscreens. Large screen devices like laptops and desktop computers often come with hardware like a mouse or a trackpad. It’s tempting to equate small screens with touch and large screens with pointers.
But the reality is more nuanced. Some laptops have touch screen capabilities. Users can interact with either a touchscreen or a trackpad or both. Likewise it’s possible to use an external keyboard or mouse with a touchscreen device like a tablet.
Instead of trying to infer the input mechanism from screen size, use media features in CSS.
Pointer
You can test for three possible values with the pointer media feature: none, coarse, and fine.
If a browser reports a pointer value of none the user might be using a keyboard to interact with your website.
If a browser reports a pointer value of coarse it means the primary input mechanism isn’t very accurate. A finger on a touchscreen is a coarse pointer.
If a browser reports a pointer value of fine it means the primary input mechanism is capable of fine-grained control. A mouse or stylus is a fine pointer.
You can adjust the size of your interface elements to suit the pointer value. Try visiting this website in different kinds of devices to see how the interface adapts.
In this example, buttons are made larger for coarse pointers:
button {
padding: 0.5em 1em;
}
@media (pointer: coarse) {
button {
padding: 1em 2em;
}
}
It’s possible to also make elements smaller for fine pointers but be careful about doing this:
{% Compare 'worse' %}
@media (pointer: fine) {
button {
padding: 0.25em 0.5em;
}
}
{% endCompare %}
Even if someone has a primary input mechanism capable of fine-grained control, think twice before reducing the size of interactive elements. Fitts’s Law still applies. A smaller target requires more concentration even with a fine pointer. A larger target area benefits everyone regardless of pointing device.
Any pointer
The pointer media feature reports the fineness of the primary input mechanism. But many devices have more than one input mechanism. It’s possible that someone is interacting with your website using both a touchscreen and a mouse at the same time.
The any-pointer differs from the pointer media feature in that it reports if any pointing device passes the test.
An any-pointer value of none means that no pointing device is available.
An any-pointer value of coarse means that at least one pointing device is not very accurate. But that might not be the primary input mechanism.
An any-pointer value of fine means that at least one pointing device is capable of fine-grained control. But again, this might not be the primary input mechanism.
Because the any-pointer media query will report a positive result if any of the input mechanisms pass the test, it’s possible for a browser to report a result for any-pointer: fine and also report a result for any-pointer: coarse. In that case the order of your media queries matters. The last one will take precedence.
In this example, if the device has both a fine and a coarse input mechanism, the coarse styles are applied.
@media (any-pointer: fine) {
button {
padding: 0.5em 1em;
}
}
@media (any-pointer: coarse) {
button {
padding: 1em 2em;
}
}
Hover
The hover media feature reports on whether the primary input mechanism can hover over elements. This usually means there’s some kind of cursor on the screen being controlled by a mouse or a trackpad.
Unlike the pointer media feature which differentiates between fine and coarse pointers, the hover media feature is binary. If the primary input device is capable of hovering over elements it will report a value of hover. Otherwise the value is none.
In this example, some supplementary icon is available on hover but only if the primary input device is capable of hovering over an element.
button .extra {
visibility: visible;
}
@media (hover: hover) {
button .extra {
visibility: hidden;
}
button:hover .extra {
visibility: visible;
}
}
{% Codepen { user: 'web-dot-dev', id: 'VwMrYav', height: 200, theme: 'dark', tab: 'result' } %}
If you use your mouse to hover over that button, the icon will appear. But if you use your keyboard to tab to the button, the icon remains invisible. When you use the keyboard, you’re focusing rather than hovering. A desktop device with a mouse attached will report that the primary input mechanism is capable of hovering, which is true. But anyone using a keyboard while a mouse is attached won’t get the benefit of the :hover styles. So it’s a good idea to combine :hover and :focus styles to cover both interactions.
button .extra {
visibility: visible;
}
@media (hover: hover) {
button .extra {
visibility: hidden;
}
button:is(:hover, :focus) .extra {
visibility: visible;
}
}
{% Codepen { user: 'web-dot-dev', id: 'LYzOEZE', height: 200, theme: 'dark', tab: 'result' } %}
Even if the primary input device is capable of hovering over elements, be careful about hiding information behind a hover interaction. The information becomes less discoverable. Don’t use hover to hide important information or an important interface element.
Any hover
The hover media query only reports on the primary input mechanism. Some devices have multiple input mechanisms: touchscreen, mouse, keyboard, trackpad.
Just as any-pointer reports on any of the input mechanisms, any-hover will be true if any of the available input mechanisms are capable of hovering over elements.
If you decided to reverse the logic in the previous example, you could make the hover styles the default and then remove them if any-hover has a value of none.
button .extra {
visibility: hidden;
}
button:hover .extra,
button:focus .extra {
visibility: visible;
}
@media (any-hover: none) {
button .extra {
visibility: visible;
}
}
{% Codepen { user: 'web-dot-dev', id: 'Bawmyzd', height: 200, theme: 'dark', tab: 'result' } %}
On a device that has no input mechanism capable of hovering, the extra icon is always visible.
Virtual keyboards
People use cursors and fingers to explore interfaces but when it comes time to enter information, they need a keyboard. That’s fine if there’s a physical keyboard attached to their device, but if they’re using a touchscreen device it’s a little more complicated. These devices provide on-screen virtual keyboards.
Input types
Unlike a physical keyboard, virtual keyboards can be tailored to match the expected input. If you provide information about the expected input, devices can serve up the most appropriate virtual keyboard.
HTML5 input types are a great way of describing your input elements. The type attribute accepts values such as email, number, tel, url, and more.
<label for="email">Email</label>
<input type="email" id="email">
<label for="number">Number</label>
<input type="number" id="number">
<label for="tel">Tel</label>
<input type="tel" id="tel">
<label for="url">URL</label>
<input type="url" id="url">
{% Codepen { user: 'web-dot-dev', id: 'zYEPxBX', height: 400, theme: 'dark', tab: 'html, result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/xdbqHZ1fp2O8FpwXWQAH.mp4", controls=true, loop=true %}
Input modes
{% BrowserCompat 'html.global_attributes.inputmode' %}
The inputmode attribute gives you fine-grained control over virtual keyboards. For example, whereas there’s one input type with a value of number, you can use the inputmode attribute to differentiate between whole numbers and decimals.
If you’re asking for a whole number, like somebody’s age, use inputmode="numeric".
<label for="age">Age</label>
<input type="number" id="age" inputmode="numeric">
If you’re asking for a number that includes decimal places, like a price, use inputmode="decimal".
<label for="price">Price</label>
<input type="number" id="price" inputmode="decimal">
{% Codepen { user: 'web-dot-dev', id: 'mdBqyrO', height: 300, theme: 'dark', tab: 'html, result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/gr0tQXIZDgRNFcExwN8W.mp4", controls=true, loop=true %}
Autocomplete
{% BrowserCompat 'html.global_attributes.autocomplete' %}
Nobody likes filling in forms. As a designer, you can improve the experience for your users by enabling them to automatically fill in form fields. The autocomplete attribute provides you with a host of options for improving contact forms, log-in forms, and checkout forms.
<label for="name">Name</label>
<input type="text" id="name" autocomplete="name">
<label for="country">Country</label>
<input type="text" id="country" autocomplete="country">
<label for="email">Email</label>
<input type="email" id="email" autocomplete="email">
{% Codepen { user: 'web-dot-dev', id: 'oNGogYX', height: 400, theme: 'dark', tab: 'html, result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/6CvIrXjmzMbXX4CNuzCe.mp4", controls=true, loop=true %}
These HTML attributes—type, inputmode, and autocomplete—are small additions to your form fields, but they can make a big difference to the user experience. By anticipating and responding to your user’s device capabilities, you are empowering your users. For more in-depth information, there’s a course dedicated to helping you learn forms.
Next up in this course, it’s time to examine some common interface patterns.
{% Assessment 'interaction' %}
Internationalization
The World Wide Web is available to everyone in the world—it's right there in the name! That means that your website is potentially available to anyone who has access to the internet, regardless of where they are, what device they're using, or what languages they speak.
The goal of responsive design is to make your content available to everyone. Applying that same philosophy to human languages is the driving force behind internationalization—preparing your content and designs for an international audience.
Logical properties
English is written from left to right and top to bottom, but not all languages are written this way. Some languages like Arabic and Hebrew read from right to left, and some Japanese typefaces read vertically instead of horizontally. To accommodate these writing modes, logical properties were introduced in CSS.
If you write CSS, you may have used directional keywords like "left", "right", "top", and "bottom." Those keywords refer to the physical layout of the user's device.
Logical properties, on the other hand, refer to the edges of a box as they relate to the flow of content. If the writing mode changes, CSS written with logical properties will update accordingly. That's not the case with directional properties.
Whereas the directional property margin-left always refers to the margin on the left side of a content box,
the logical property margin-inline-start refers to the margin on the left side of a content box in a left-to-right language,
and the margin on the right side of a content box in a right-to-left language.
In order for your designs to adapt to different writing modes, avoid directional properties. Use logical properties instead.
{% Compare 'worse' %}
.byline {
text-align: right;
}
{% endCompare %}
{% Compare 'better' %}
.byline {
text-align: end;
}
{% endCompare %}
When CSS has a specific directional value like left or right,
there's a corresponding logical property. Where once we had margin-left now we also have margin-inline-start.
In a language like English where text flows from left to right,
inline-start corresponds to "left" and inline-end corresponds to "right".
Likewise, in a language like English where the text is written from top to bottom,
block-start corresponds to "top" and block-end corresponds to "bottom."
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/9EcUvMxy9T10EzY4U95b.webp", alt="Latin, Hebrew and Japanese are shown rendering placeholder text within a device frame. Arrows and colors follow the text to help associate the 2 directions of block and inline.", width="800", height="577" %}
If you use logical properties in your CSS, you can use the same stylesheet for translations of your pages. Even if your pages are translated into languages written from right to left or bottom to top, your design will adjust accordingly. You don't need to make separate designs for each language. By using logical properties, your design will respond to every writing mode. That means your design can reach more people without you having to spend time making separate designs for every language.
Modern CSS layout techniques like grid and flexbox use logical properties by default.
If you think in terms of inline-start and block-start instead of left and top
then you'll find these modern techniques easier to understand.
Take a common pattern like an icon next to some text or a label next to a form field. Instead of thinking "the label should have a margin on the right," think "the label should have a margin on the end of its inline axis."
{% Compare 'worse' %}
label {
margin-right: 0.5em;
}
{% endCompare %}
{% Compare 'better' %}
label {
margin-inline-end: 0.5em;
}
{% endCompare %}
{% Codepen { user: 'web-dot-dev', id: 'gOxXOLK', height: 300, theme: 'dark', tab: 'css,result' } %}
If that page is translated into a right-to-left language,
the styles won't need to be updated.
You can mimic the effect of seeing your pages in a right-to-left language by using the dir attribute on your html element.
A value of ltr means "left to right." A value of "rtl" means "right to left."
{% Codepen { user: 'web-dot-dev', id: 'mdMqdOx', height: 300, theme: 'dark', tab: 'html,result' } %}
If you'd like to experiment with all the permutations of document directions (the block axis) and writing modes (the inline axis), here's an interactive demonstration.
{% Codepen { user: 'argyleink', id: 'vYNwbgM', height: 800, theme: 'dark', tab: 'result' } %}
Identify page language
It's a good idea to identify the language of your page by using the lang attribute on the html element.
<html lang="en">
That example is for a page in English. You can be even more specific. Here's how you declare that a page is using US English:
<html lang="en-us">
Declaring the language of your document is useful for search engines. It's also useful for assistive technologies like screen readers and voice assistants. By providing language metadata you're helping these kinds of speech synthesizers pronounce your content correctly.
The lang attribute can go on any HTML element, not just html. If you switch languages in your web page, indicate that change.
In this case, one word is in German:
<p>I felt some <span lang="de">schadenfreude</span>.</p>
Identify a linked document's language
There's another attribute called hreflang which you can use on links.
The hreflang takes the same language code notation as the lang attribute and describes the linked document's language.
If there's a translation of your entire page available in German, link to it like this:
<a href="/path/to/german/version" hreflang="de">German version</a>
If you use text in German to describe the link to the German version, use both hreflang and lang.
Here, the text "Deutsche Version" is marked up as being in the German language, and the destination link is also marked up as being in German:
<a href="/path/to/german/version" hreflang="de" lang="de">Deutsche Version</a>
You can also use the hreflang attribute on the link element. This goes in the head of your document:
<link href="/path/to/german/version" rel="alternate" hreflang="de">
But unlike the lang attribute, which can go on any element, hreflang can only be applied to a and link elements.
Consider internationalization in your design
When you're designing websites that will be translated into other languages and writing modes, think about these factors:
- Some languages, like German, have long words in common usage. Your interface needs to adapt to these words so avoid designing narrow columns. You can also use CSS to introduce hyphens.
- Make sure your
line-heightvalues can accommodate characters like accents and other diacritics. Lines of text that look fine in English might overlap in a different language. - If you're using a web font, make sure it has a range of characters broad enough to cover the languages you'll be translating into.
- Don't create images that have text in them. If you do, you'll have to create separate images for each language. Instead, separate the text and the image, and use CSS to overlay the text on the image.
Think internationally
Attributes like lang and hreflang make your HTML more meaningful for internationalization.
Likewise, logical properties make your CSS more adaptable.
If you're used to thinking in terms of top, bottom, left, and right,
it might be hard to start thinking of block start, block end, inline start and inline end instead.
But it's worth it. Logical properties are key to creating truly responsive layouts.
{% Assessment 'internationalization' %}
Next, you'll learn how to approach page-level layouts, also known as macro layouts.
Introduction
Right from the start, the World Wide Web was designed to be agnostic. It doesn't matter what hardware you've got. It doesn't matter what operating system your device is running. As long as you can connect to the internet, the World Wide Web is accessible to you.
In the early days of the web, most people were using desktop computers. These days the web is available on desktops, laptops, tablets, foldable phones, fridges, and cars. People rightly expect that websites will look good no matter what device they're using. Responsive design makes this possible.
Responsive design isn't the first approach to designing websites. In the years before responsive design, web designers and developers tried many different techniques.
Fixed-width design
In the early 1990s, when the web was first becoming popular, most monitors had screen dimensions of 640 pixels wide by 480 pixels tall. These were convex cathode ray tubes, not like the flat liquid crystal displays we have now.
In the formative days of early web design, it was a safe bet to design web pages with a width of 640 pixels. But while other technologies like phones and cameras were miniaturizing, screens were getting bigger (and eventually, flatter). Before long, most screens had dimensions of 800 by 600 pixels. Web designs changed accordingly. Designers and developers started assuming that 800 pixels was a safe default.
Then the screens got bigger again. 1024 by 768 became the default. It felt like an arms race between web designers and hardware manufacturers.
Whether it was 640, 800, or 1024 pixels, choosing one specific width to design for was called fixed-width design.
If you specify a fixed width for your layout, then your layout will only look good at that specific width. If a visitor to your site has a wider screen than the width you have chosen, then there'll be wasted space on the screen. You can center the content of your pages to distribute that space more evenly (instead of having empty space on one side) but you still wouldn't be taking full advantage of the available space.
Similarly, if a visitor arrives with a narrower screen than the width you've chosen, then your content won't fit horizontally. The browser generates a crawlbar—the horizontal equivalent of a scrollbar—and the user has to move the whole page left and right to see all the content.
{% Codepen { user: 'web-dot-dev', id: 'RwZjwoQ', height: 500, theme: 'dark', tab: 'result' } %}
Liquid layouts
While the majority of designers used fixed-width layouts, some chose to make their layouts flexible. Instead of using fixed widths for your layouts you could make a flexible layout using percentages for your column widths. These designs work in more situations than a fixed-width layout that only looks right at one specific size.
These were called liquid layouts. But while a liquid layout will look good across a wide range of widths, it will begin to worsen at the extremes. On a wide screen the layout looks stretched. On a narrow screen the layout looks squashed. Both scenarios aren't ideal.
You can mitigate these problems by using min-width and max-width for your layout.
But then at any size below the minimum width or above the maximum width you've got the same issues you'd have with a fixed-width layout.
On a wide screen there'd be unused space going to waste.
On a narrow screen, the user would have to move the whole page left and right to see everything.
The word liquid is just one of the terms used to describe this kind of layout. These kinds of designs were also called fluid layouts or flexible layouts. The terminology was as fluid as the technique.
Small screens
In the 21st century, the web continued to get bigger and bigger. So did monitors. But new screens arrived that were smaller than any desktop device. With the arrival of mobile phones with fully-featured web browsers, designers faced a dilemma. How could they ensure their designs would look good on a desktop computer and a mobile phone? They needed a way of styling their content for screens as small as 240 pixels wide and as large as thousands of pixels wide.
Separate sites
One option is to make a separate subdomain for mobile visitors. But then you have to maintain two separate codebases and designs. And in order to redirect visitors on mobile devices, you'd need to do user-agent sniffing, which can be unreliable and easily spoofed. Chrome will be deprecating its user-agent string for privacy reasons. Also, there's no clear line between mobile and not-mobile. Which site do you send tablet devices to?
Adaptive layouts
Instead of having separate sites on different subdomains, you could have a single site with two or three fixed-width layouts.
When media queries first arrived in CSS, they opened the door to making layouts more flexible. But many developers were still most comfortable making fixed-width layouts. One technique involved switching between a handful of fixed-width layouts at specified widths. Some people call this an adaptive design.
Adaptive design allowed designers to provide layouts that looked good at a few different sizes, but the design never looked quite right when viewed between those sizes. The problem of excess space persisted although it wasn't as bad as in a fixed-width layout.
Using CSS media queries, you can give people the layout that's closest to their browser width. But given the variety of device sizes, chances are the layout will look less than perfect for most people.
Responsive web design
If adaptive layouts are a mashup of media queries and fixed-width layouts, responsive web design is a mashup of media queries and liquid layouts.
The term was coined by Ethan Marcotte in an article in A List Apart in 2010.
Ethan defined three criteria for responsive design:
- Fluid grids
- Fluid media
- Media queries
The layout and images of a responsive site would look good on any device. But there was one problem.
A meta element for viewport
Browsers on mobile phones had to deal with websites that were designed with fixed-width layouts for wider screens. By default mobile browsers assumed that 980 pixels was the width that people were designing for (and they weren't wrong). So even if you used a liquid layout, the browser would apply a width of 980 pixels and then scale the rendered web page down to the actual width of the screen.
If you're using responsive design you need to tell the browser not to do that scaling.
You can do that with a meta element in the head of the web page:
<meta name="viewport" content="width=device-width, initial-scale=1">
There are two values, separated by commas.
The first one is width=device-width.
This tells the browser to assume the width of the website is the same as the width of the device
(instead of assuming the width of the website is 980 pixels).
The second value is initial-scale=1.
This tells the browser how much or how little scaling to do.
With a responsive design, you don't want the browser to do any scaling at all.
With that meta element in place, your web pages are ready to be responsive.
Modern responsive design
Today we have the ability to make websites that are responsive in ways far beyond viewport sizes.
Media features give developers access to user preferences and enable customized experiences.
Container queries enable components to own their own responsive information.
The picture element empowers designers to make art-direction decisions based on screen ratios.
{% Assessment 'intro' %}
Responsive design is an exciting, growing world of possibilities. In the rest of this course, you'll learn about these technologies and how to use them to create beautiful, responsive websites for everyone.
Macro layouts
Macro layouts describe the larger, page-wide organization of your interface.
{% Img src="image/HodOHWjMnbNw56hvNASHWSgZyAf2/HoOnH9yMrSY6MhQdRNRZ.jpeg", alt="A wireframe of a two column layout, next to the same layout as one column for a narrow view.", width="800", height="501" %}
Before applying any layout, you should make sure that the flow of your content makes sense. This single column default ordering is what smaller screens will get.
<body>
<header>…</header>
<main>
<article>…</article>
<aside>…</aside>
</main>
<footer>…</footer>
</body>
{% Codepen { user: 'web-dot-dev', id: 'oNeePOX', height: 500, theme: 'dark', tab: 'result' } %}
When you arrange these individual page-level components, you're designing a macro layout: a high-level view of your page. Using media queries, you can supply rules in CSS describing how this view should adjust to different screen sizes.
{% Video src="video/HodOHWjMnbNw56hvNASHWSgZyAf2/3KENjI9FiNARctTiKDak.mp4", autoplay=true, controls=true, loop=true %}
Grid
CSS grid is an excellent tool for applying a layout to your page. In the example above, say you want a two-column layout once there's enough screen width available. To apply this two-column layout once the browser is wide enough, use a media query to define the grid styles above a specified breakpoint.
@media (min-width: 45em) {
main {
display: grid;
grid-template-columns: 2fr 1fr;
}
}
{% Aside %}
While it would make more sense to specify min-inline-size instead of min-width,
logical properties don't work in media queries yet.
{% endAside %}
{% Codepen { user: 'web-dot-dev', id: 'vYJJzMK', height: 500, theme: 'dark', tab: 'result' } %}
Flexbox
For this specific layout, you could also use flexbox. The styles would look like this:
@media (min-width: 45em) {
main {
display: flex;
flex-direction: row;
}
main article {
flex: 2;
}
main aside {
flex: 1;
}
}
{% Codepen { user: 'web-dot-dev', id: 'gOxxdym', height: 500, theme: 'dark', tab: 'result' } %}
However, the flexbox version requires more CSS. Each column has a separate rule to describe how much space it should take up. In the grid example, that same information is encapsulated in one rule for the containing element.
Do you need a media query?
You might not always need to use a media query. Media queries work fine when you're applying changes to a few elements, but if the layout needs to be updated a lot, your media queries could get out of hand with lots of breakpoints.
Say you've got a page full of card components.
The cards are never wider than 15em, and you want to put as many cards on one line as will fit.
You could write media queries with breakpoints of 30em, 45em, 60em,
and so on, but that's quite tedious and difficult to maintain.
Instead, you can apply rules so that the cards themselves automatically take up the right amount of space.
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
grid-gap: 1em;
}
{% Codepen { user: 'web-dot-dev', id: 'QWMMVPm', height: 500, theme: 'dark', tab: 'result' } %}
You can achieve a similar layout with flexbox. In this case, if there are not enough cards to fill the final row, the remaining cards will stretch to fill the available space rather than lining up in columns. If you want to line up rows and columns, then use grid.
.cards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1em;
}
.cards .card {
flex-basis: 15em;
flex-grow: 1;
}
{% Codepen { user: 'web-dot-dev', id: 'abyyaMg', height: 500, theme: 'dark', tab: 'result' } %}
By applying some smart rules in flexbox or grid, it's possible to design dynamic macro layouts with minimal CSS and without any media queries. That's less work for you—you're making the browser do the calculations instead. To see some examples of modern CSS layouts that are fluid without requiring media queries, see 1linelayouts.com.
{% Assessment 'macro-layouts' %}
Now that you've got some ideas for page-level macro layouts, turn your attention to the components within the page. This is the realm of micro layouts.
Media features
Responsive design wouldn't be possible without media queries. Before media queries there was no way of knowing what kind of device people were using to visit your website. Designers had to make assumptions. Those assumptions were encoded into fixed-width designs or liquid layouts.
That all changed with the introduction of media queries. For the first time designers could meet people halfway. Designers could adjust their layouts to respond to people's devices.
Remember, a media query comprises an optional media type and a mandatory media feature. There hasn't been much change in media types over the years. There are still just four possible values:
@media all
@media screen
@media print
@media speech
Media features, on the other hand, have expanded greatly. Designers now can meet users beyond halfway, adapting designs to fit far more than just screen size. {% BrowserCompat 'css.at-rules.media' %}
Viewport dimensions
By far the most popular media queries on the web are the ones dealing with viewport width. But even here, you're presented with a choice. You can use the max-width media feature to apply styles below a certain width, or you can use the min-width media feature to apply styles above a certain width.
main {
display: grid;
grid-template-columns: 2fr 1fr;
}
@media (max-width: 45em) {
main {
display: block;
}
}
@media (min-width: 45em) {
main {
display: grid;
grid-template-columns: 2fr 1fr;
}
}
Personally, I like to use min-width. I apply layout styles in an additive way. I introduce new layout rules at each breakpoint instead of undoing previous rules.
This additive approach also works for height. Using min-height you can introduce more rules as more viewport height becomes available. For example, you might have a header element that you want to anchor to the top of the browser window if there's enough vertical space.
@media (min-height: 30em) {
header {
position: fixed;
}
}
{% Codepen { user: 'web-dot-dev', id: 'PoJbdaN', height: 500, theme: 'dark', tab: 'result' } %}
But you can use the max-height media feature if you prefer. Here, the header is anchored by default, but the stickiness is removed if there isn't enough vertical space.
header {
position: fixed;
}
@media (max-height: 30em) {
header {
position: static;
}
}
Choosing between min- and max- prefixes doesn't just apply to width and height. The aspect-ratio media feature offers the same choice. It also offers an unprefixed version if you want to apply styles at an exact ratio of width to height.
@media (min-aspect-ratio: 16/9) {
// The ratio of width to height is at least 16 by 9.
}
@media (max-aspect-ratio: 16/9) {
// The ratio of width to height is less than 16 by 9.
}
@media (aspect-ratio: 16/9) {
// The ratio of width to height is exactly 16 by 9.
}
Providing different styles for different aspect ratios could quickly get out of hand. If you don't need that fine-grained level of control, the orientation media feature might better suit your needs. It has two possible values: portrait or landscape.
@media (orientation: portrait) {
// The width is less than the height.
}
@media (orientation: landscape) {
// The width is greater than the height.
}
{% Codepen { user: 'web-dot-dev', id: 'qBXVowV', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/gtBzPVsBu5mtnX8RuSqJ.mp4", controls=true, loop=true %}
Even though the terms "portrait" and "landscape" are most often used to refer to the orientation of mobile devices, the orientation media feature isn't device-specific. A full-screen browser window on a typical laptop will have an orientation value of landscape.
Displays
Different displays have different pixel densities, measured in dpi, dots per inch. You can adjust your styles for different pixel densities using the resolution media feature. Like aspect-ratio, resolution comes in three varieties: minimum, maximum, and exact.
@media (min-resolution: 300dpi) {
// The display has a pixel density of at least 300 dots per inch.
}
@media (max-resolution: 300dpi) {
// The display has a pixel density less than 300 dots per inch.
}
@media (resolution: 300dpi) {
// The display has a pixel density of exactly 300 dots per inch.
}
You may never need to use any resolution media queries. Pixel density is usually only an issue for images, and responsive images are a way of dealing with pixel density directly in HTML.
On the other hand, CSS is where you define your animations and transitions. You can adjust those animations and transitions to respond to different refresh rates using the update media feature. This media feature reports one of three values: none, slow, and fast.
An update value of none means there's no refresh rate. A printed page, for example, can't be updated.
An update value of slow means the display can't refresh quickly. An e-ink display is one example of a screen with a slow refresh rate.
An update value of fast means the display refreshes fast enough to handle animations and transitions.
@media (update: fast) {
a {
transition-duration: 0.4s;
transition-property: transform;
}
a:hover {
transform: scale(150%);
}
}
Not all aspects of the display are related to hardware capabilities. In the module on theming, you saw how to define properties in a web app manifest file. One of those properties is called display, and you can give it a value of fullscreen, standalone, minimum-ui, or browser. The corresponding display-mode media feature allows you to tailor your styles for these different options.
Let's say you've provided a display value of standalone in your web app manifest. If someone adds your site to their home screen, the site will launch without any browser interface. You might decide to display a back button for those users.
button.back {
display: none;
}
@media (display-mode: standalone) {
button.back {
display: inline;
}
}
Color
There are numerous media features for querying the color capabilities of a device. If you want to adjust your styles for any display that only outputs shades of grey, you can use the monochrome media feature without any value.
@media (monochrome) {
body {
color: black;
background-color: white;
}
}
There's a corresponding color media feature.
@media (color) {
body {
color: maroon;
background-color: linen;
}
}
For color screens, you can write queries with the media features color-gamut, color-index, or dynamic-range. All of them report specific details about the color capabilities of the screen.
In this example, color values update in response to the dynamic-range media feature, which reports the combination of maximum brightness, color depth, and contrast ratio of the display. The possible values are standard or high. A high-definition screen that reports a dynamic-range value of high is given a different color space using the new CSS color() function.
.neon-red {
color: hsl(355 100% 95%);
}
@media (dynamic-range: high) {
.neon-red {
color: color(display-p3 1 0 0);
}
}
{% Codepen { user: 'web-dot-dev', id: 'BawQOPQ', height: 400, theme: 'dark', tab: 'result' } %}
Interaction
Media features can also report the kind of input mechanism used to interact with your site: hover, any-hover, pointer, and any-pointer. See the module on interaction for more details.
Preferences
There are a range of media queries that allow you to respond to user preferences: prefers-color-scheme, prefers-contrast, and prefers-reduced-motion. See the modules on theming and accessibility for more details.
You can combine media features in one media query. You could scope your animation styles so that they're only applied if the device has a fast refresh rate and the user hasn't expressed a preference for reduced motion.
@media (update: fast) and (prefers-reduced-motion: no-preference) {
a {
transition-duration: 0.4s;
transition-property: transform;
}
a:hover {
transform: scale(150%);
}
}
More media features
There are more media features on the way.
The forced-colors and inverted-colors media features will report whether a device is using a restricted or an inverted color palette.
A scripting media feature will allow you to adjust your CSS based on the availability of JavaScript.
A media feature called prefers-reduced-data will allow users to specify that they're on a metered connection so you can send smaller or fewer assets.
Other proposals are still being formulated. In the next and final module, you'll learn about a proposal for a media feature that handles different screen configurations.
{% Assessment 'media-features' %}
Media queries
Designers can adjust their designs to accommodate users. The clearest example of this is the form factor of a user's device; its width, the device aspect ratio, and so on. Using media queries, designers can respond to these different form factors.
Media queries are initiated with the @media keyword (a CSS at-rule), and can be used for a variety of use cases.
Target different types of output
Websites are often displayed on screens but CSS can also be used to style websites for other outputs too. You might want your web pages to look one way on a screen but different when printed out. Querying media types makes this possible.
In this example, the background color is set to grey. But if the page is printed, the background color should be transparent. This saves the user's printer ink.
body {
color: black;
background-color: grey;
}
@media print {
body {
background-color: transparent;
}
}
You can use the @media at-rule in your stylesheet like that,
or you can make a separate stylesheet and use the media attribute on a link element:
<link rel="stylesheet" href="global.css">
<link rel="stylesheet" href="print.css" media="print">
If you don't specify any media type for your CSS,
it will automatically have a media type value of all. These two blocks of CSS are equivalent:
body {
color: black;
background-color: white;
}
@media all {
body {
color: black;
background-color: white;
}
}
These two lines of HTML are also equivalent:
<link rel="stylesheet" href="global.css">
<link rel="stylesheet" href="global.css" media="all">
Query conditions
You can add conditions to media types. These are called media queries. The CSS is applied only if the media type matches and the condition is also true. These conditions are called media features.
This is the syntax for media queries:
@media type and (feature)
You can use media queries on a link element if your styles are in a separate stylesheet:
<link rel="stylesheet" href="specific.css" media="type and (feature)">
Let's say you want to apply different styles depending on whether the browser window is in landscape mode
(the viewport width is greater than its height) or portrait mode
(the viewport height is greater than its width).
There's a media feature called orientation you can use to test that:
@media all and (orientation: landscape) {
// Styles for landscape mode.
}
@media all and (orientation: portrait) {
// Styles for portrait mode.
}
Or if you prefer to have separate stylesheets:
<link rel="stylesheet" href="landscape.css" media="all and (orientation: landscape)">
<link rel="stylesheet" href="portrait.css" media="all and (orientation: portrait)">
In this case the media type is all. Because that's the default value, you can leave it out if you want:
@media (orientation: landscape) {
// Styles for landscape mode.
}
@media (orientation: portrait) {
// Styles for portrait mode.
}
Or using separate stylesheets:
<link rel="stylesheet" href="landscape.css" media="(orientation: landscape)">
<link rel="stylesheet" href="portrait.css" media="(orientation: portrait)">
{% Codepen { user: 'web-dot-dev', id: 'qBXVowV', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/gtBzPVsBu5mtnX8RuSqJ.mp4", autoplay=true, controls=true, loop=true %}
While using separate stylesheets for different media types—like print—might be okay,
it's probably not a good idea to use a separate stylesheet for every media query. Use @media at-rules instead.
Adjust styles based on viewport size
For responsive design, one of the most useful media features involves the dimensions of the browser viewport.
To apply styles when the browser window is wider than a certain width, use min-width.
@media (min-width: 400px) {
// Styles for viewports wider than 400 pixels.
}
Use the max-width media feature to apply styles below a certain width:
@media (max-width: 400px) {
// Styles for viewports narrower than 400 pixels.
}
You can use any CSS length units in your media queries.
If your content is mostly image-based, pixels might make the most sense.
If your content is mostly text-based,
it probably makes more sense to use a relative unit that's based on text size, like em or ch:
@media (min-width: 25em) {
// Styles for viewports wider than 25em.
}
You can also combine media queries to apply more than one condition.
Use the word and to combine your media queries:
@media (min-width: 50em) and (max-width: 60em) {
// Styles for viewports wider than 50em and narrower than 60em.
}
{% Codepen { user: 'web-dot-dev', id: 'GRvOxaw', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/eDf5v1QMwLX19pcJAgYE.mp4", autoplay=true, controls=true, loop=true %}
Choose breakpoints based on the content
The point at which a media feature condition becomes true is called a breakpoint. It's best to choose your breakpoints based on your content rather than popular device sizes, as those are subject to change with every technology release cycle.
In this example, 50em is the point at which the lines of text become uncomfortably long.
So a breakpoint is created to make the interface more legible.
Using the column-count property, the text is divided into two columns from that point on.
@media (min-width: 50em) {
article {
column-count: 2;
}
}
{% Codepen { user: 'web-dot-dev', id: 'oNeodgd', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/mrQlcyih7IcZxswI1lMx.mp4", autoplay=true, controls=true, loop=true %}
Combinations
You can use media queries based on the height of the viewport, not just the width. This could be useful for optimizing interface content "above the fold". In the previous example, if readers are using a wide but short browser window, they have to scroll down one column and then scroll back up to get to the top of the second column. It would be safer to only apply the columns when the viewport is both wide enough and tall enough.
You can combine media queries so that the styles only apply when all the conditions are true.
In this example, the viewport must be at least 50em wide and 60em tall in order for the column styles to be applied.
Those breakpoints were chosen based on the amount of content.
@media (min-width: 50em) and (min-height: 60em) {
article {
column-count: 2;
}
}
{% Codepen { user: 'web-dot-dev', id: 'JjyOvdM', height: 1000, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/oKRBxhrHzAzebYwxptsv.mp4", autoplay=true, controls=true, loop=true %}
These examples show how media queries can be used to adapt designs to the form factor of a user's device, but these just scratch the surface of possibilities. Media queries can go far beyond width and height, accessing user preferences for accessibility features and theme colors. Using media queries to make layout adjustments is a great start to a responsive design, which builds on these features and more.
{% Assessment 'media-queries' %}
Micro layouts
When we think of layouts, we often think of page-level designs. But smaller components within the page can have their own self-contained layouts.
Ideally, these component-level layouts will adjust themselves automatically, regardless of their position on the page. There may be situations where you don't know if a component will be placed into the main content column or the sidebar or both. Without knowing for sure where a component will end up, you need to make sure that the component can adjust itself to its container.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/hAz94zpnzIQYHjamxtYK.png", alt="A two column layout, one wide and one narrow. The media objects are laid out differently depending on whether they're in the wide or narrow column.", width="800", height="410" %}
{% Aside %} In the future, creating components that can adapt to their container will become much easier with the implementation of Container Queries. For now, however, you need to consider the existing ways to create reusable and responsive micro-layouts. You can find a preview of how Container Queries fit into existing responsive design methods at the end of this module. {%endAside%}
Grid
CSS grid isn't just for page-level layouts. It also works well for the components that live within them.
In this example, the ::before and ::after pseudo-elements
create decorative lines on either side of a heading.
The heading itself is a grid container.
The individual elements are laid out so that the lines always fill the available space.
h1 {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1em;
}
h1::before,
h1::after {
content: "";
border-top: 0.1em double black;
align-self: center;
}
{% Codepen { user: 'web-dot-dev', id: 'rNzYvxm', height: 200, theme: 'dark', tab: 'result' } %}
Learn how to inspect grid layouts in Chrome DevTools.
Flexbox
As the name suggests, flexbox allows you to make your components flexible. You can declare which elements in your component should have a minimum or maximum size and let the other elements flex to fit accordingly.
In this example, the image takes up one quarter of the available space and the text takes up the other three quarters. But the image never gets larger than 200 pixels.
.media {
display: flex;
align-items: center;
gap: 1em;
}
.media-illustration {
flex: 1;
max-inline-size: 200px;
}
.media-content {
flex: 3;
}
{% Codepen { user: 'web-dot-dev', id: 'qBXVYZo', height: 300, theme: 'dark', tab: 'result' } %}
Learn how to inspect flexbox layouts in Chrome DevTools.
Container queries
Flexbox allows you to design from the content out. You can specify the parameters of your elements (how narrow they should get, how wide they should get) and let the browser figure out the final implementation.
But the component itself has no awareness of its context. It doesn't know if it's being used in the main content or in a sidebar. This can make component layouts trickier than page layouts. To be able to apply contextually relevant styles, your components need to know more than the size of the viewport they are inside.
With page layouts, you do know the width of the container because the container is the browser viewport; media queries report the dimensions of the page-level container.
Now there's an upcoming CSS technology that reports the dimensions of any parent container: container queries.
{% Aside 'caution' %} Container queries are a new experimental technology that isn't widely available in browsers. To test the code below, and see the example working, enable container queries in Chrome.
Go to chrome://flags/, search for Container Queries, and enable the #enable-container-queries flag.
With the flag enabled, you can inspect and debug container queries in Chrome DevTools.
{% endAside %}
To start, define which elements will act as containers.
main,
aside {
container-type: inline-size;
}
This means that you want to be able to query the inline dimension. For English-language documents, that's the horizontal axis. You're going to change styles based on the width of the container.
If a component is inside one of those containers, you can apply styles in a way that's quite similar to media queries.
.media-illustration {
max-width: 200px;
margin: auto;
}
@container (min-width: 25em) {
.media {
display: flex;
align-items: center;
gap: 1em;
}
.media-illustration {
flex: 1;
}
.media-content {
flex: 3;
}
}
If a media object is inside a container that's narrower than 25em,
the flexbox styles aren't applied. The image and text appear are ordered vertically.
But if the containing element is wider than 25em, the image and text appear side by side.
Container queries allow you to style components in an independent way. The width of the viewport is no longer what matters. You can write rules based on the width of the containing element.
{% Img src="image/HodOHWjMnbNw56hvNASHWSgZyAf2/819QcTZ9zW0uVk8tTdGi.png", alt="Two containers of different sizes.", width="800", height="624" %}
Combining queries
You can use media queries for the page layout, and container queries for the components within the page.
Here the overall structure of the page has a main element and an aside element.
There are media objects within both elements.
<body>
<main>
<div class="media">…</div>
<div class="media">…</div>
</main>
<aside>
<div class="media">…</div>
</aside>
</body>
A media query applies a grid layout to the main and aside elements when the viewport is wider than 45em.
@media (min-width: 45em) {
body {
display: grid;
grid-template-columns: 3fr 1fr;
}
}
The container query rule for the media objects remains the same:
only apply a horizontal flexbox layout if the containing element is wider than 25em.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/hAz94zpnzIQYHjamxtYK.png", alt="A two column layout, one wide and one narrow. The media objects are laid out differently depending on whether they're in the wide or narrow column.", width="800", height="410" %}
{% Aside 'caution' %}
This demo won't work in every browser. In Google Chrome you can find the #enable-container-queries flag at chrome://flags.
{% endAside %}
{% Codepen { user: 'web-dot-dev', id: 'RwZjyRv', height: 500, theme: 'dark', tab: 'result' } %}
Container queries are a game-changer for micro layouts. Your components can be self-contained, independent of the browser viewport.
{% Assessment 'micro-layouts' %}
Previously, you learned about page-level macro layouts. Now you know about component-level micro layouts. Next, you'll dive deeper into the very building blocks of your content. You'll learn how to make your images responsive. But now let's explore responsive typography.
The picture element
The previous module demonstrated how the srcset attribute allows you to provide different-sized versions of the same image. The browser can then decide which is the right version to use. If you want to change the image completely, you'll need the picture element.
In the same way that srcset builds upon the src attribute, the picture element builds upon the img element. The picture element wraps around an img element.
<picture>
<img src="image.jpg" alt="A description of the image.">
</picture>
If there is no img element nested inside the picture element, the picture element won't work.
Like the srcset attribute, the picture element will update the value of the src attribute in that img element. The difference is that where the srcset attribute gives suggestions to the browser, the picture element gives commands. This gives you control.
source
You can specify multiple source elements inside a picture element, each one with its own srcset attribute. The browser then executes the first one that it can.
Image formats
In this example, there are three different images in three different formats:
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="A description of the image."
width="300" height="200" loading="lazy" decoding="async">
</picture>
The first source element points to an image in the new AVIF format. If the browser is capable of rendering AVIF images, then that's the image file it chooses. Otherwise, it moves on to the next source element.
The second source element points to an image in the WebP format. If the browser is capable of rendering WebP images, it will use that image file.
Otherwise the browser will fall back to the image file in the src attribute of the img element. That image is a JPEG.
This means you can start using new image formats without sacrificing backward compatibility.
In that example, the type attribute told the browser which kind of image format was specified.
Image sizes
As well as switching between image formats, you can switch between image sizes. Use the media attribute to tell the browser how wide the image will be displayed. Put a media query inside the media attribute.
<picture>
<source srcset="large.png" media="(min-width: 75em)">
<source srcset="medium.png" media="(min-width: 40em)">
<img src="small.png" alt="A description of the image."
width="300" height="200" loading="lazy" decoding="async">
</picture>
Here you're telling the browser that if the viewport width is wider than 75em it must use the large image. Between 40em and 75em the browser must use the medium image. Below 40em the browser must use the small image.
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/tXME65OZikEMwvPAEgX2.mp4", controls=true, loop=true %}
This is different to using the srcset and sizes attributes on the img element. In that case you're providing suggestions to the browser. The source element is more like a command than a suggestion.
You can also use the pixel density descriptor inside the srcset attribute of a source element.
<picture>
<source srcset="large.png 1x" media="(min-width: 75em)">
<source srcset="medium.png 1x, large.png 2x" media="(min-width: 40em)">
<img src="small.png" alt="A description of the image." width="300" height="200" loading="lazy" decoding="async"
srcset="small.png 1x, medium.png 2x, large.png 3x">
</picture>
In that example you're still telling the browser what to do at different breakpoints, but now the browser has the option to choose the most appropriate image for the device's pixel density.
{% Codepen { user: 'web-dot-dev', id: 'PoJYrNB', height: 500, theme: 'dark', tab: 'result' } %}
Cropping
If you only need to serve differently sized versions of the same image, srcset is your best option. But if an image doesn't look good at smaller sizes, you can try making a cropped version of the image instead.
The different images might have different width and height ratios to suit their context better. For example, on a mobile browser you may want to serve a crop that's narrow and tall, whereas on a desktop browser, you might want to serve a crop that's wide and short.
Here's an example of a hero image that changes its contents and its shape based on the viewport width. Add width and height attributes to each source element.
<picture>
<source srcset="full.jpg" media="(min-width: 75em)" width="1200" height="500">
<source srcset="regular.jpg" media="(min-width: 50em)" width="800" height="400">
<img src="cropped.jpg" alt="A description of the image." width="400" height="400" loading="eager" decoding="sync">
</picture>
{% Codepen { user: 'web-dot-dev', id: 'eYGOwzp', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/WZLJGIUHqlP1j9s9aJaj.mp4", controls=true, loop=true %}
Remember that you can't change the alt attribute depending on the image source. You'll need to write an alt attribute that describes both the full size image and the cropped image.
You probably won't need to use the picture element for most of your responsive images—the srcset and sizes attributes on the img element cover a lot of use cases. But for those situations when you need more fine-grained control, the picture element is a powerful tool.
There's one kind of image where you might not need either solution: icons. That's what's next.
{% Assessment 'picture' %}
Responsive images
Text on the web automatically wraps at the edge of the screen so that it doesn't overflow. It's different with images. Images have an intrinsic size. If an image is wider than the screen, the image will overflow, causing a horizontal scrollbar to appear.
Fortunately, you can take measures in CSS to stop this from happening.
Constrain your images
In your stylesheet, you can declare that images should never be rendered at a size wider than their containing element using max-inline-size.
{% BrowserCompat 'css.properties.max-inline-size' %}
img {
max-inline-size: 100%;
block-size: auto;
}
{% Aside %}
You can use max-width instead of max-inline-size if you prefer, but remember it's good to get in the habit of thinking in terms of logical properties.
{% endAside %}
You can apply the same rule to other kinds of embedded content too, like videos and iframes.
img,
video,
iframe {
max-inline-size: 100%;
block-size: auto;
}
With that rule in place, browsers will automatically scale down images to fit on the screen.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/RvqAjVGiroJDuBBpYXYV.png", alt="Two screenshots; the first shows an image expanding past the browser width; the second shows the same image constrained within the browser viewport.", width="800", height="763" %}
Adding a block-size value of auto means that the aspect-ratio of the images will remain constant.
Sometimes the dimensions of an image might be out of your control—if an image is added through a content management system, for example. If your design calls for a images to have an aspect ratio that's different to the image's real dimensions, use the aspect-ratio property in CSS.
img {
max-inline-size: 100%;
block-size: auto;
aspect-ratio: 2/1;
}
But then the browser might squash or stretch the image to make it fit your preferred aspect ratio.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/GNEkQTnMZTde5QB2mkvh.jpg", alt="Profile of a happy-looking handsome dog with a ball in its mouth, but the image is squashed.", width="800", height="400" %}
To prevent that happening, use the object-fit property.
{% BrowserCompat 'css.properties.object-fit' %}
An object-fit value of contain tells the browser to preserve the image's aspect ratio, even if that means leaving empty space above and below.
img {
max-inline-size: 100%;
block-size: auto;
aspect-ratio: 2/1;
object-fit: contain;
}
An object-fit value of cover tells the browser to preserve the image's aspect ratio, even if that means cropping the image at the top and bottom.
img {
max-inline-size: 100%;
block-size: auto;
aspect-ratio: 2/1;
object-fit: cover;
}
If the cropping at the top and bottom evenly is an issue, use the object-position CSS property to adjust the focus of the crop. {% BrowserCompat 'css.properties.object-position' %}
You can make sure the most important image content is still visible.
img {
max-inline-size: 100%;
block-size: auto;
aspect-ratio: 2/1;
object-fit: cover;
object-position: top center;
}
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/mZDZFob5QnBl5YmAHh2A.jpg", alt="Profile of a happy-looking handsome dog with a ball in its mouth; the image has only been cropped at the bottom.", width="800", height="400" %}
Deliver your images
Those CSS rules tell the browser how you'd like images to be rendered. You can also provide hints in your HTML about how the browser should handle those images.
Sizing hints
If you know the dimensions of the image you should include width and height attributes. Even if the image is rendered at a different size (because of your max-inline-size: 100% rule), the browser still knows the width to height ratio and can set aside the right amount of space. This will stop your other content jumping around when the image loads.
<img
src="image.png"
alt="A description of the image."
width="300"
height="200"
>
Loading hints
Use the loading attribute to tell the browser whether to delay loading the image until it is in or near the viewport. For images below the fold, use a value of lazy. The browser won't load lazy images until the user has scrolled far down enough that the image is about to come into view. If the user never scrolls, the image never loads.
<img
src="image.png"
alt="A description of the image."
width="300"
height="200"
loading="lazy"
>
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/jazsyaOjXfHS0g8yUijR.mp4", controls=true, loop=true %}
For a hero image above the fold, loading should not be used. If your site automatically applies the loading="lazy" attribute, you can often set the eager attribute (which is the default) to prevent this from being applied:
<img
src="hero.jpg"
alt="A description of the image."
width="1200"
height="800"
loading="eager"
>
Fetch Priority
For important images-such as the LCP image, you can further prioritize the loading using Fetch Priority by setting the fetchpriority attribute to high:
<img
src="hero.jpg"
alt="A description of the image."
width="1200"
height="800"
loading="eager"
fetchpriority="high"
>
This will tell the browser to fetch the image right away, and at high priority, rather than waiting until the browser has completed layout when images are normally fetched.
But remember: when you ask the browser to prioritize downloading one resource—like an image—the browser will have to de-prioritize another resource such as a script or a font file. Only set fetchpriority="high" on an image if it is truly vital.
Preloading hints
Some images may not be available in the initial HTML—if they are added by JavaScript, or as a background image in CSS. You can also use preload to allow these important images to be fetched ahead of time. This can be combined with the fetchpriority attribute for really important images:
<link rel="preload" href="hero.jpg" as="image" fetchpriority="high">
Again this should be used sparingly to avoid overriding the browsers prioritisation heuristics too much, which may result in performance degredation.
Preloading responsive images based on srcset (which is discussed below) via the imagesrcset and imagesizes attributes is more advanced and is supported in some browsers, but not all:
By excluding the href fallback you can ensure browsers that do not support this do not preload the incorrect image.
Preloading based on different image formats based on browser support of those images is not currently supported and may result in extra downloads.
The ideal is to avoid preload where possible, and have the image available in the initial HTML, to avoid repeating code, and to allow access to the full range of options the browser supports.
Image decoding
There's also a decoding attribute you can add to img elements. You can tell the browser that the image can be decoded asynchronously. The browser can then prioritize processing other content.
<img
src="image.png"
alt="A description of the image."
width="300"
height="200"
loading="lazy"
decoding="async"
>
You can use the sync value if the image itself is the most important piece of content to prioritize.
<img
src="hero.jpg"
alt="A description of the image."
width="1200"
height="800"
loading="eager"
decoding="sync"
>
The decoding attribute will not change how fast the image decodes, but merely whether the browser waits for this image decoding to happen before rendering other content.
In most cases this will have little impact, however in certain scenarios it can allow the image or content to be displayed slightly faster. For example, for a large document with lots of elements that take time to render, and with large images that take a while to decode, setting sync on important images will tell the browser to wait for the image and render both at once. Alternatively, setting async can allow the content to be displayed faster without waiting for the image decode.
However, the better option is usually to try to avoid excessive DOM sizes and ensure responsive images are used to reduce decoding time meaning the decoding attribute will have little effect.
Responsive images with srcset
Thanks to that max-inline-size: 100% declaration, your images will never break out of their containers. But even if it looks fine to have a large image that shrinks to fit, it won't feel fine. If someone uses a small screen device on a low bandwidth network, they'll download unnecessarily large images.
If you make multiple versions of the same image at different sizes, you can let the browser know about them using the srcset attribute.
Width descriptor
You can pass in a list of values separated by commas. Each value should be the URL of an image followed by a space followed by some metadata about the image. This metadata is called a descriptor.
In this example, the metadata describes the width of each image using the w unit. One w is one pixel.
<img
src="small-image.png"
alt="A description of the image."
width="300"
height="200"
loading="lazy"
decoding="async"
srcset="small-image.png 300w,
medium-image.png 600w,
large-image.png 1200w"
>
The srcset attribute doesn't replace the src attribute. Instead the srcset attribute complements the src attribute. You still need to have a valid src attribute, but now the browser can replace its value with one of the options listed in the srcset attribute.
The browser won't download the larger images unless they're needed. That saves bandwidth.
Sizes
If you're using the width descriptor, you must also use the sizes attribute to give the browser more information. This tells the browser what size you expect the image to be displayed under different conditions. Those conditions are specified in a media query.
The sizes attribute takes a comma-separated list of media queries and image widths.
{% Aside %} It feels strange to have CSS media queries inside an HTML element. It spoils the separation of presentation and structure. But it's the only way to provide the necessary sizing information in time to be effective. {% endAside %}
<img
src="small-image.png"
alt="A description of the image."
width="300"
height="200"
loading="lazy"
decoding="async"
srcset="small-image.png 300w,
medium-image.png 600w,
large-image.png 1200w"
sizes="(min-width: 66em) 33vw,
(min-width: 44em) 50vw,
100vw"
>
In this example, you're telling the browser that above a viewport width of 66em to display the image no wider than one third of the screen (inside a three column layout, for example).
For viewport widths between 44em and 66em, display the image at half the width of the screen (a two column layout).
For anything below 44em display the image at the full width of the screen.
This means that the biggest image won't necessarily be used for the widest screen. A wide browser window that can display a multi-column layout will use an image that fits in one column. That image might be smaller than an image used for a single-column layout on a narrower screen.
{% Codepen { user: 'web-dot-dev', id: 'XWerLJm', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/MkUAq5XQyjlUXLvkT4jL.mp4", controls=true, loop=true %}
Pixel density descriptor
There's another situation where you might want to provide multiple versions of the same image.
Some devices have high-density displays. On a double-density display you can pack two pixels worth of information into the space of one pixel. This keeps images looking sharp on those kinds of displays.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/OtyGs9nZser6u0DrFYBq.png", alt="Two versions of the same image of a happy-looking handsome dog with a ball in its mouth, one image looking crisp and the other looking fuzzy.", width="800", height="275" %}
Use the density descriptor to describe the pixel density of the image in relationship to the image in the src attribute. The density descriptor is a number followed by the letter x: 1x, 2x, etc.
<img
src="small-image.png"
alt="A description of the image."
width="300"
height="200"
loading="lazy"
decoding="async"
srcset="small-image.png 1x,
medium-image.png 2x,
large-image.png 3x"
>
If small-image.png is 300 by 200 pixels in size, and medium-image.png is 600 by 400 pixels in size, then medium-image.png can have 2x after it in the srcset list.
You don't have to use whole numbers. If another version of the image is 450 by 300 pixels in size, you can describe it with 1.5x.
{% Aside %} You can use either width descriptors or density descriptors, but not both together. {% endAside %}
Presentational images
Images in HTML are content. That's why you always provide an alt attribute with a description of the image for screen readers and search engines.
If you embed an image that is purely a visual flourish without any meaningful content, use an empty alt attribute.
<img
src="flourish.png"
alt=""
width="400"
height="50"
>
You must still include the alt attribute. A missing alt attribute is not the same as an empty alt attribute. An empty alt attribute conveys to a screen reader that this image is presentational.
Ideally, your presentational or decorative images shouldn't be in your HTML at all. HTML is for structure. CSS is for presentation.
Background images
Use the background-image property in CSS to load presentational images.
element {
background-image: url(flourish.png);
}
You can specify multiple image candidates using the image-set function for background-image.
The image-set function in CSS works a lot like the srcset attribute in HTML. Provide a list of images with a pixel density descriptor for each one.
element {
background-image: image-set(
small-image.png 1x,
medium-image.png 2x,
large-image.png 3x
);
}
The browser will choose the most appropriate image for the device's pixel density.
There are many factors to consider when you're adding images to your site:
Reserving the right space for each image. Figuring out how many sizes you need. Deciding whether the image is content or decorative.
It's worth spending the time to get images right. Poor image strategies are responsible for frustration and annoyance for users. A good image strategy makes your site feel snappy and sharp, regardless of the user's device or network connection.
There's one more HTML element in your toolkit to help you exercise more control over your images: the picture element.
{% Assessment 'images' %}
Screen configurations
Responsive web design was in many ways a reaction to mobile phones. Before smartphones appeared, very few people seriously considered how websites should look and feel on handheld devices. That changed with the meteoric rise of mobile phones featuring built-in web browsers.
Responsive web design encouraged a mindset that questioned assumptions. Whereas previously it was common to assume that a website would only be viewed on a desktop computer, now it's standard practice to design that same website for phones and tablets as well. In fact, mobile usage has now eclipsed desktop usage on the web.
This responsive mindset will serve you well for the future. It's entirely possible that your websites will be viewed on devices and screens that we can't even imagine today. And this mindset extends beyond screens. Even now people are using devices with no screens to access your content. Voice assistants can use your websites if you are using a strong foundation of semantic HTML.
There's experimentation in the world of screens too. There are devices on the market today with foldable screens. That introduces some challenges for your designs.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/f4vIlX35hgoM8l0cWE9a.jpg", alt="A montage of foldable phones in different configurations.", width="800", height="449" %}
Dual screens
Users of foldable devices can choose whether they want their web browser to occupy just one of the screens or span across both screens. If the browser spans both screens, then the website on display will be broken up by the hinge between the two screens. It doesn't look great.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/gRdTgrPMARsnUl1iLmie.png", alt="A website spanning across two screens. The horizontal flow of text is interrupted by the hinge between the screens.", width="800", height="545" %}
Viewport segments
There's an experimental media feature designed to detect if your website is being displayed on a dual-screen device. The proposed name of the media feature is viewport-segments. There are two varieties: horizontal-viewport-segments and vertical-viewport-segments.
{% Aside 'caution' %}
The viewport-segments feature is an experimental proposal and the syntax may change. The syntax has already changed from its initial proposal of a spanning media feature.
{% endAside %}
If the horizontal-viewport-segments media feature reports a value of 2 and vertical-viewport-segments reports a value of 1 that means the hinge on the device runs from top to bottom, splitting your content into two side-by-side panels.
@media (horizontal-viewport-segments: 2) and (vertical-viewport-segments: 1) {
// Styles for side-by-side screens.
}
If the vertical-viewport-segments media feature reports a value of 2 and horizontal-viewport-segments reports a value of 1 then the hinge runs from side to side, dividing your content into two panels, one on top of the other.
@media (vertical-viewport-segments: 2) and (horizontal-viewport-segments: 1) {
// Styles for stacked screens.
}
If both vertical-viewport-segments and horizontal-viewport-segments report a value of 1 this means the website is being displayed on just one screen, even if the device has more than one screen. This is equivalent to not using any media query.
Environment variables
The viewport-segments media feature by itself won't help you design around that annoying hinge. You need a way of knowing the size of the hinge. That's where environment variables can help.
Environment variables in CSS allow you to factor awkward device intrusions into your styles. For example, you can design around the "notch" on the iPhone X using the environment values safe-area-inset-top, safe-area-inset-right, safe-area-inset-bottom and safe-area-inset-left. These keywords are wrapped in an env() function.
body {
padding-top: env(safe-area-inset-top);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
}
Environment variables work like custom properties. This means you can pass in a fallback option in case the environment variable doesn't exist.
body {
padding-top: env(safe-area-inset-top, 1em);
padding-right: env(safe-area-inset-right, 1em);
padding-bottom: env(safe-area-inset-bottom, 1em);
padding-left: env(safe-area-inset-left, 1em);
}
For those environment variables to work on the iPhone X, update the meta element that specifies viewport information:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
Now your page layout will take up the entire viewport and safely pad the document with device-provided inset values.
For foldable screens, six new environment variables are being proposed: viewport-segment-width, viewport-segment-height, viewport-segment-top, viewport-segment-left, viewport-segment-bottom, viewport-segment-right.
{% Aside 'caution' %} Remember, this is just at the proposal stage right now. The specific syntax for the environment variables may well change. {% endAside %}
{% Aside %}
It might seem a little strange to see names proposed with left, right, top, and bottom. If you're using logical properties you would expect terms like inline-start, inline-end, block-start, and block-end. In this instance though, the names refer to the hardware's physical properties, regardless of the writing mode used by the website being displayed.
{% endAside %}
Here's an example of a layout with two columns, one wider than the other.
@media (min-width: 45em) {
main {
display: flex;
flex-direction: row;
}
main article {
flex: 2;
}
main aside {
flex: 1;
}
}
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/VYjqUCss2sJ6MT8b7BHe.png", alt="The layout is split across two screens with the hinge interrupting the wider column.", width="800", height="545" %}
For dual screens with a vertical hinge, set the first column to be the width of the first screen and the second column to be the width of the second screen.
@media (horizontal-viewport-segments: 2) and (vertical-viewport-segments: 1) {
main article {
flex: 1 1 env(viewport-segment-width 0 0);
}
main aside {
flex: 1;
}
}
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/BUXrXQRSkvXz7TTzWFLx.png", alt="The layout is split evenly across two screens with no visible interruption.", width="800", height="545" %}
Treat dual screens as an opportunity. Perhaps one screen could be used to display scrollable text content while the other displays a fixed element like an image or a map.
The future
Will foldable displays become the next big thing? Who knows. No one could've predicted how popular mobile devices would become so it's worth having an open mind about future form factors.
Above all, it's worth ensuring that your websites can respond to whatever the future may bring. That's what responsive design gives you: not just a set of practical techniques, but a mindset that will serve you well as you build the web of tomorrow.
{% Assessment 'screen-configurations' %}
Theming
Even branding can be responsive. You can adjust the presentation of your website to match the user's preference. But first, here's how to extend your website's branding to include the browser itself.
Customize the browser interface
Some browsers allow you to suggest a theme color based on your website's palette.
The browser's interface adapts to your suggested color. Add the color in a meta element named theme-color in the head of your pages.
<meta name="theme-color" content="#00D494">
{% Aside %} It feels a little strange to put styling information like this in HTML rather than CSS, but this allows the browser to update its interface as soon as the page is loading rather than waiting for the CSS. {% endAside %}
You can update the value of theme-color using JavaScript. But use this power wisely.
It can be overwhelming for users if their browser's color scheme changes too often.
Think about subtle ways to adjust the theme color. If the changes are too jarring, users will leave in annoyance.
You can also specify a theme color in a web app manifest file. This is a JSON file with metadata about your website.
Link to the manifest file from the head of your documents. Use a link element with a rel value of manifest.
<link rel="manifest" href="/manifest.json">
In the manifest file, list your metadata using key/value pairs.
{
"short_name": "Clearleft",
"name": "Clearleft design agency",
"start_url": "/",
"background_color": "#00D494",
"theme_color": "#00D494",
"display": "standalone"
}
If a visitor decides to add your website to their home screen, the browser will use the information in your manifest file to display an appropriate shortcut.
{% Aside %} Find out more about how to add a web app manifest. {% endAside %}
Provide a dark mode
Many operating systems allow users to specify a preference for a light or a dark color palette,
which is a good idea to optimize your site to your user's theme preferences.
You can access this preference in a media feature called prefers-color-scheme.
@media (prefers-color-scheme: dark) {
// Styles for a dark theme.
}
{% Codepen { user: 'web-dot-dev', id: 'wvrwLgN', height: 400, theme: 'dark', tab: 'css, result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/hpMyDvtHUyLPz0ltO6SS.mp4", controls=true, loop=true %}
Specify theme colors with the prefers-color-scheme media feature within the meta element.
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
You can also use the prefers-color-scheme media feature inside SVG.
If you use an SVG file for your favicon, it can be adjusted for dark mode.
Thomas Steiner wrote about
prefers-color-scheme in SVG favicons for dark mode icons.
Theming with custom properties
If you use the same color values in multiple places throughout your CSS, it could be quite tedious to repeat all your selectors within a prefers-color-scheme media query.
body {
background-color: white;
color: black;
}
input {
background-color: white;
color: black;
border-color: black;
}
button {
background-color: black;
color: white;
}
@media (prefers-color-scheme: dark) {
body {
background-color: black;
color: white;
}
input {
background-color: black;
color: white;
border-color: white;
}
button {
background-color: white;
color: black;
}
}
Use CSS custom properties to store your color values. Custom properties work like variables in a programming language. You can update the value of a variable without updating its name.
If you update the values of your custom properties within a prefers-color-scheme media query,
you won't have to write all your selectors twice.
html {
--page-color: white;
--ink-color: black;
}
@media (prefers-color-scheme: dark) {
html {
--page-color: black;
--ink-color: white;
}
}
body {
background-color: var(--page-color);
color: var(--ink-color);
}
input {
background-color: var(--page-color);
color: var(--ink-color);
border-color: var(--ink-color);
}
button {
background-color: var(--ink-color);
color: var(--page-color);
}
{% Codepen { user: 'web-dot-dev', id: 'qBPWzrj', height: 440, theme: 'dark', tab: 'css,result' } %}
See building a color scheme for more advanced examples of theming with custom properties.
Images
If you are using SVGs in your HTML, you can apply custom properties there too.
svg {
stroke: var(--ink-color);
fill: var(--page-color);
}
Now your icons will change their colors along with the other elements on your page.
If you want to tone down the brightness of your photographic images when displayed in dark mode, you can apply a filter in CSS.
@media (prefers-color-scheme: dark) {
img {
filter: brightness(.8) contrast(1.2);
}
}
{% Codepen { user: 'web-dot-dev', id: 'PoJYrpy', height: 600, theme: 'dark', tab: 'result' } %}
For some images, you might want to swap them out completely in dark mode.
For example, you might want to show a map with a darker color scheme.
Use the <picture> element containing a <source> element with the prefers-color-scheme media query.
<picture>
<source srcset="darkimage.png" media="(prefers-color-scheme: dark)">
<img src="lightimage.png" alt="A description of the image.">
</picture>
{% Codepen { user: 'web-dot-dev', id: 'MWEgMmw', height: 600, theme: 'dark', tab: 'html,result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/8Sz0XmotUrwnbprrHtoZ.mp4", controls=true, loop=true %}
Forms
Browsers provide a default color palette for form fields. Let the browser know that your site offers both a dark and a light mode. That way, the browser can provide the appropriate default styling for forms.
Add this to your CSS:
html {
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
You can also use HTML. Add this in the head of your documents:
<meta name="supported-color-schemes" content="light dark">
{% Codepen { user: 'web-dot-dev', id: 'dyVbBWZ', height: 400, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/6YuAM0ZCNhGNAjpI8fyT.mp4", controls=true, loop=true %}
Use the accent-color property in CSS to style checkboxes, radio buttons, and some other form fields.
html {
accent-color: red;
}
It's common for dark themes to have subdued brand colors. You can update the accent-color value for dark mode.
html {
accent-color: red;
}
@media (prefers-color-scheme: dark) {
html {
accent-color: pink;
}
}
It makes sense to use a custom property for this so you can keep all your color declarations in one place.
html {
color-scheme: light;
--page-color: white;
--ink-color: black;
--highlight-color: red;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
--page-color: black;
--ink-color: white;
--highlight-color: pink;
}
}
html {
accent-color: var(--highlight-color);
}
body {
background-color: var(--page-color);
color: var(--ink-color);
}
{% Codepen { user: 'web-dot-dev', id: 'WNZeqjB', height: 400, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/Dl00EnG4mwTbSQIkYBF9.mp4", controls=true, loop=true %}
{% Aside %} For more on tinting elements with theme colors, see the section on more tinting. {% endAside %}
Providing a dark mode is just one example of adapting your site to suit your user's preferences. Next you'll learn how to make your site adaptable to all sorts of accessibility considerations.
{% Assessment 'theming' %}
Typography
If you don't specify any styles for your text, browsers will apply their own default styles. These are called User Agent stylesheets, and may vary from browser to browser. Users can set their own preferences for displaying text too.
If you don't specify a line length, browsers will wrap lines of text at the edge of the screen. So text on the web is responsive by default—it flows to fit the user's viewport.
But just because text fits on a screen doesn't mean it's comfortable to read. Good typography is all about presenting your text in an appropriate way. There's more to typography than choosing suitable fonts to use. You need to consider the user's preferences, the size of the text, line length, and the distance between the lines of text.
{% Aside %} You may even need to consider the primary language of your user, as you may need to leave room for accents and other special characters. Learn more about this in the internationalization module. {% endAside %}
Text size
It's difficult to know what size text on the web should be.
If someone is using a small screen, it might be a safe bet that their screen will be fairly close to their eyes—a hand's length away.
But as screens get larger and larger, it's harder to make that connection. A laptop-size screen will probably be fairly near to the viewer, but a widescreen desktop monitor is around the same size as a television screen. People sit an arm's length away from a desktop screen but they sit much further away from a television.
Still, while you can't know for certain how far away someone is from a screen, you can try to use text sizes that will hopefully turn out to be appropriate. Use smaller text sizes for smaller screens and larger text sizes for larger screens.
You can use media queries to change the font-size property as the screen size gets wider.
@media (min-width: 30em) {
html {
font-size: 125%;
}
}
@media (min-width: 40em) {
html {
font-size: 150%;
}
}
@media (min-width: 50em) {
html {
font-size: 175%;
}
}
@media (min-width: 60em) {
html {
font-size: 200%;
}
}
{% Codepen { user: 'web-dot-dev', id: 'WNZeBWY', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/cH8IU9hV7FGCgN0roNiU.mp4", controls=true, loop=true %}
Scaling text
Switching between fixed text sizes at specific breakpoints is quite jumpy. A more responsive approach is to let the user's device width influence the text size.
The vw unit in CSS stands for “viewport width.” Connecting font sizes to the
viewport's width means that the text will grow and shrink in proportion to the browser width. This makes it difficult to predict what the text size will be at any specific width, but you know that the text size will be appropriate for the user's browser width.
It's important that you don't use the vw by itself in a font-size declaration.
{% Compare 'worse' %}
html {
font-size: 2.5vw;
}
{% endCompare %}
If you do, the user won't be able to resize the text. The text will be resizable if you mix in a relative unit—like em, rem or ch. The CSS calc() function is perfect for this.
{% Compare 'better' %}
html {
font-size: calc(0.75rem + 1.5vw);
}
{% endCompare %}
Let the browser do the math. This makes it difficult to predict exactly what the text size will be at any specific width, but you know that the text size will be in the right range. The user's browser takes care of figuring out the exact text size calculations.
{% Codepen { user: 'web-dot-dev', id: 'zYEOQQG', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/x5h7TqkGHJIPa61wPvAs.mp4", controls=true, loop=true %}
But now there's a possibility that the text will get too small on narrow screens and too big on wide screens.
Clamping text
You probably don't want your text to shrink and grow to extremes. You can control where the scaling starts and ends using the CSS clamp() function. This “clamps” the scaling to a specific range.
The clamp() function is like the calc() function but it takes three values. The middle value is the same as what you pass to calc(). The opening value specifies the minimum size, in this case 1rem so as to not go below the user's preferred font size. The closing value specifies the maximum size.
html {
font-size: clamp(1rem, 0.75rem + 1.5vw, 2rem);
}
{% Codepen { user: 'web-dot-dev', id: 'wvrwbbx', height: 500, theme: 'dark', tab: 'result' } %}
Now the text size shrinks and grows in proportion to the user's screen but the text size will never go below 1rem or above 2rem.
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/vVwP2L0z90zXL28iPYL4.mp4", controls=true, loop=true %}
Line length
The web is not print, but we can learn lessons from the world of print and apply them on the web.
In his classic book The Elements of Typographic Style, Robert Bringhurst had this to say on line length (or measure):
Anything from 45 to 75 characters is widely regarded as a satisfactory line length for a single-column page set in a serifed text face in a text size. The 66-character line (counting both letters and spaces) is widely regarded as ideal. For multiple column work, a better average is 40 to 50 characters.
You can't set a line length directly in CSS. There is no line-length property. But you can stop text from getting too wide by limiting how wide the container can be. The max-inline-size property is perfect for this.
Don't set your line-lengths with a fixed unit like px. Users can scale their font size up and down and your line lengths should adjust accordingly. Use a relative unit like rem or ch.
{% Compare 'worse' %}
article {
max-inline-size: 700px;
}
{% endCompare %}
{% Compare 'better' %}
article {
max-inline-size: 66ch;
}
{% endCompare %}
Using ch units for width will cause new lines to wrap at the 66th character at that font size.
{% Codepen { user: 'web-dot-dev', id: 'MWEgddN', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/9Nxi6HuU28aQhxtzXq7l.mp4", controls=true, loop=true %}
Line height
Although there is no line-length property in CSS, there is a line-height property.
Shorter lines of text can have larger line-height values. But if you use large line-height values for long lines of text, it's hard for the reader's eye to move from the end of one line to the start of the next line.
article {
max-inline-size: 66ch;
line-height: 1.65;
}
blockquote {
max-inline-size: 45ch;
line-height: 2;
}
{% Codepen { user: 'web-dot-dev', id: 'oNGvRrZ', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/di6psFbT6JhlwjtMm68j.mp4", controls=true, loop=true %}
Use unitless values for your line-height declarations. This ensures that the line height is relative to the font-size.
{% Compare 'worse' %}
line-height: 24px;
{% endCompare %}
{% Compare 'better' %}
line-height: 1.5;
{% endCompare %}
Combinations and scale
Remember to prioritize hierarchy as you build your user interfaces for better clarity and page flow. A great way to do this is with a typography scale built into your design system.
Web fonts
A typeface is like a voice for your words. For the longest time on the web there were very few font options. System fonts were the only options. But now you can choose a web font that matches the feel of your content.
Use @font-face to tell browsers where to find your web font files. Use woff2 as your web font format. It's well supported and has the best performance gains.
@font-face {
font-family: Roboto;
src: url('/fonts/roboto-regular.woff2') format('woff2');
}
body {
font-family: Roboto, sans-serif;
}
But every web font file you add could potentially degrade the user experience as it increases page load time. Remember, design isn't just about how the final pixels look. How fast those pixels get painted is a critical part of the user experience. An experience that feels fast is a good user experience.
Font loading
You can request that browsers start downloading a font file as soon as possible. Add a link element to the head of your document that references your web font file. A rel attribute with a value of preload tells the browser to prioritize that file. An as attribute with a value of font tells the browser what kind of file this is. The type attribute allows you to be even more specific.
<link href="/fonts/roboto-regular.woff2" type="font/woff2"
rel="preload" as="font" crossorigin>
You need to include the crossorigin attribute even if you are hosting the font files yourself.
Use the CSS font-display property to tell the browser how to manage the switchover from a system font to a web font. You could choose to show no text at all until the web font is loaded. You could choose to display the system font immediately and then switch over to the web font once it loads. Both strategies have their downsides. If you wait until the web font is downloaded before showing any text, users may find themselves staring at a blank page for a frustratingly long time. If you show the text in a system font first and then switch over to the web font, users may experience a jarring shifting of content on the page.
{% Aside %}
You can mitigate the jarring effect of text shifting around by using size-adjust in your @font-face rule.
{% endAside %}
A good compromise is to wait for a short while before displaying any text. If the web font loads before that time is up, the text is displayed using the web font with no content shifts. If the web font still hasn't loaded after the time is up, the text is displayed using the system font so at least the user can read the content.
Use a font-display value of swap if you still want the web font to replace the system font whenever the web font finally loads.
body {
font-family: Roboto, sans-serif;
font-display: swap;
}
Use a font-display value of fallback if you want to stick with the system font once text has been rendered.
body {
font-family: Roboto, sans-serif;
font-display: fallback;
}
Variable fonts
If you are using lots of different weights or styles of the same typeface, you may end up using lots of separate font files—a separate font file for each weight or style.
Variable fonts solve this problem by using one file. Instead of having separate files for regular, bold, extra bold, and so on, a variable font file is responsive. It contains all the information it needs to be displayed across a spectrum of weights or styles.
{% Img src="image/tcFciHGuF3MxnTr1y5ue01OGLBn2/Ecr5godvTKunVXP7W8aU.png", alt="The letter 'A' shown in different weights.", width="800", height="218" %}
This means that a single variable font file is larger than a single regular font file, but a single variable font file will probably be smaller than multiple regular font files. If you're using lots of different weights, a variable font could give you a big performance gain.
{% Aside %}
If you're styling elements using the system-ui value for font-face property, you might get all the benefits of variable fonts without downloading any font files. More and more system fonts are now variable fonts.
{% endAside %}
Good typography on the web isn't just about the type choices that you make as a designer. Responsive typography is also about respecting the user's device and network connection. The end result is a design that feels right no matter how it's being viewed.
Now that you've mastered responsive text, it's time to dive into responsive images.
{% Assessment 'typography' %}
User interface patterns
A design viewed on a small screen shouldn't look like a shrunk-down version of a large-screen layout. Likewise, a design viewed on a large screen shouldn't look like a blown-up version of a small-screen layout. Instead, the design needs to be flexible enough to adapt to all screen sizes. A successful responsive design makes the most of every form factor.
This means that some interface elements may need to look quite different depending on the context they're viewed in. You might need to apply very different CSS to the same HTML codebase to make the most of different screen sizes. That can be quite a design challenge!
Here are some common challenges that you might face.
Navigation
Displaying a list of navigation links is quite straightforward on a large screen. There's plenty of space to accommodate those links.
On a small screen, space is at a premium. When you're designing for this situation, it's tempting to hide navigation behind a button. The problem with this solution is that users have to then take two steps to get anywhere: open the menu, then choose an option. Until the menu is opened, the user is left wondering "where can I go?”
Try to find a strategy that avoids hiding your navigation. If you have a relatively small number of items, you can style the navigation to look good on small screens.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/s1fif52p8RcE758OkSvB.png", alt="The same website with five navigation links viewed in a mobile browser and viewed in a tablet browser. The navigation is visible on both devices.", width="800", height="409" %}
That pattern won't scale if your navigation has lots of links. The navigation will look cluttered if the links wrap onto two or three lines on a small screen.
One possible solution is to keep links on one line but truncate the list at the edge of the screen. Users can swipe horizontally to reveal the links that aren't immediately visible. This is the overflow pattern.
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/jNM3gZrkksJc6banrxcE.mp4", controls=true %}
The advantage of this technique is that it scales to any device width and any number of links. The disadvantage is that users might miss links that aren't initially visible. If you use this technique for your main navigation, make sure the first few links are the most important ones and visually indicate that there are more items in the list. The previous example uses a gradient for this indicator.
As a last resort you could choose to have your navigation hidden by default and provide a toggle mechanism for users to show and hide the contents. This is called progressive disclosure.
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/DiGHrNu300JMj8i9X01U.png", alt="The same website with five navigation links viewed in a mobile browser and viewed in a tablet browser. The navigation is visible on the tablet, but hidden on the mobile device.", width="800", height="409" %}
{% Img src="image/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/cyuj5HvE6x9c8OBiM527.png", alt="Three unlabelled icons: the first is three horizontal lines; the second is three by three grid; the third is three circles arranged vertically.", width="640", height="240" %}
An unlabelled icon is "mystery meat” navigation—users won't know what's in there until they bite into it. Provide a text label to let users know what the button will reveal.
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/jzRDWQyKNcV1JNBU6mYS.mp4", controls=true, loop=true %}
Carousels
That said, when space is at a premium, carousels can prevent a page getting too long and cluttered. You could employ a hybrid approach: show content in a carousel for small screens, but display the same content in a grid for larger screens.
For narrow screens, display items in a row using flexbox. The row of items will extend beyond the edge of the screen. Use overflow-x: auto to allow horizontal swiping.
@media (max-width: 50em) {
.cards {
display: flex;
flex-direction: row;
overflow-x: auto;
scroll-snap-type: inline mandatory;
scroll-behavior: smooth;
}
.cards .card {
flex-shrink: 0;
flex-basis: 15em;
scroll-snap-align: start;
}
}
{% Aside %}
The logical version of overflow-x is overflow-inline which would better match with the value for scroll-snap-type. This example uses the physical version for better cross-browser support.
{% endAside %}
The scroll-snap properties ensure that the items can be swiped in a way that feels smooth. Thanks to scroll-snap-type: inline mandatory, the items snap into place.
When the screen is large enough—wider than 50em in this case—switch over to grid and display the items in rows and columns, without hiding anything.
@media (min-width: 50em) {
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
}
}
{% Codepen { user: 'web-dot-dev', id: 'XWeNPzY', height: 500, theme: 'dark', tab: 'result' } %}
{% Video src="video/KT4TDYaWOHYfN59zz6Rc0X4k4MH3/jw00jziSxqLm3JBYRk5o.mp4", controls=true %}
Crucially, an item in the carousel view doesn't take up the full width. If it did, there would be no indication that more content is available beyond the edge of the viewport.
Carousels are another example of the overflow pattern in action. If you have many items that people can browse through, you could keep using the overflow pattern even on large screens, including televisions. This media scroller uses multiple carousels to manage a significant amount of options.
Again, the scroll-snap properties ensure that the interaction feels smooth. Also, notice that the images in the carousel have loading="lazy" applied to them. In this case, the images aren't below the fold, they're beyond the edge, but the same principle applies: if the user never swipes as far as that item, the image won't be downloaded, saving on bandwidth.
{% Codepen { user: 'argyleink', id: 'bGgyOGP', height: 500, theme: 'dark', tab: 'result' } %}
{% Aside %} The previous example shows a more complete demo, you can read more about it in the article building a media scroller component. {% endAside %}
With the addition of JavaScript, you can add interactive controls to a carousel. You could even make it automatically cycle through items. But think long and hard before doing that—autoplaying might work if the carousel is the only content on the page, but an autoplaying carousel is incredibly annoying if someone is trying to interact with other content (like reading text, for example). You can read more carousel best practices.
{% CodePattern '/web-vitals-patterns/carousels/carousel-autoplay/' %}
Data tables
The table element is perfect for structuring tabular data; rows and columns of related information. But if the table gets too large, it could break your small-screen layout.
You can apply the overflow pattern to tables. In this example, the table is wrapped in a div with a class of table-container.
.table-container {
max-inline-size: 100%;
overflow-x: auto;
scroll-snap-type: inline mandatory;
scroll-behavior: smooth;
}
.table-container th,
.table-container td {
scroll-snap-align: start;
padding: var(--metric-box-spacing);
}
{% Codepen { user: 'web-dot-dev', id: 'MWEbqQQ', height: 500, theme: 'dark', tab: 'result' } %}
Guidelines
The overflow pattern is a good compromise for small screens but make sure it's clear that the off-screen content is reachable. Consider placing a shadow or a gradient over the edge where content is truncated.
Progressive disclosure is a useful way to save space, but be careful about using it for very important content. It's better suited to secondary actions. Ensure the interface element that triggers the disclosure is clearly labeled—don't rely on iconography alone.
Design for smaller screens first. It's easier to adapt small-screen designs to larger screens than the other way around. If you design for a large screen first, there's a danger that your small-screen design will feel like an afterthought.
For more layout and UI element patterns, explore the web.dev Patterns section.
When you're adapting interface elements to different screen sizes, media queries are very useful for figuring out the dimensions of the device. But media features like min-widthand min-height are just the beginning. Next, you'll discover a whole host of other media features.
Learn Forms
This course breaks down HTML forms into easy to understand pieces. Over the next few modules, you'll learn how an HTML form works and how to use them effectively in your projects. Use the menu pane by the Learn Forms logo to navigate the modules.
You'll learn how to build a basic HTML form, about HTML form elements, styling forms, help users re-entering data, ensuring the form is accessible, and secure, how to test your forms, and about specific form types.
Each module is full of interactive demos and self-assessments for you to test your knowledge.
This course is suitable for beginners and advanced HTML developers. The first few modules help you get started with building HTML forms, the other modules give more detail. You can go through the series from start to finish to get a general understanding of HTML forms, or pick a specific module you want to learn more about.
Prerequisites
Before starting this course, you should know about HTML. For those new to web development, check out the intro to HTML course from MDN to learn all about how to write markup.
Demos
For most demos, we use CodePen.
Looking at the demos, you might wonder where the styles come from. We use a general stylesheet included in all demos, so only the relevant styles are shown there. In CodePen, you can select included CSS files from Settings.
Form controls and form fields
A form control is an element that enables user interaction and data entry or selection:
an <input>, <select>, <textarea> or <button>.
Sometimes form field is used to refer to form controls,
in particular elements for text entry: <input> and <textarea>.
Here's what you'll learn
{% include 'partials/course-index.njk' %}
So, are you ready to learn about forms? Let's get started.
{% Aside %}
This course is written and edited by forms community experts:
Michael Scharnagl,
Sam Dutton,
and Rachel Andrew. Rob Dodson and Jiwoong Lee shepherded the UX and production build, along with help from Ewa Gasperowicz and Sam Thorogood.
{% endAside %}
Accessibility
The form you build is for people. People use different devices. Some use a mouse, some a touch device, some the keyboard, some a device controlled by eye movements. Some use a screen reader, some a small screen, some use text enlargement software. Everybody wants to use your form. Learn how to make your form accessible and usable for everyone.
Ensure users understand the purpose of a form field
There are many form controls you can choose from.
What do they all have in common?
Every form control must have an associated <label> element.
The <label> element describes the purpose of a form control.
The <label> text is visually associated with the form control, and read out by screen readers.
In addition, tapping or clicking the <label> focuses the associated form control,
making it a larger target.
{% Aside %}
The next time you add a form control, add the <label> first. Think about the purpose of the
form control, and describe the purpose to the user. Make it easy for people to enter valid data
quickly and accurately.
{% endAside %}
Use meaningful HTML to access built-in browser features
In theory, you could build a form using only <div> elements.
You can even make it look like a native <form>.
What's the problem with using
non-semantic elements?
Built-in form elements provide a lot of built-in features. Let's have a look at an example.
{% Codepen { user: 'web-dot-dev', id: '2f56a723b5045ea990d38c2c170c3037', height: 450, tab: 'html,result' } %}
Visually, the <input> (the first one in the example) and the <div> look the same.
You can even insert text for both, as the <div> has a
contenteditable attribute.
There are lots of differences, though, between using an appropriate <input> element and a <div>
looking like an <input>.
A screen reader user doesn't recognize the <div> as an input element,
and isn't able to complete the form.
All the screen reader user hears is 'Name',
with no indication that the element is a form control for adding text.
Clicking on <div>Name</div> doesn't focus the <div> that goes with it, whereas the <label> and
the <input> are connected by using the for and id attributes.
After submitting the form, the data entered in the <div> isn't included in the request.
While attaching the data with JavaScript is possible,
an <input> does that by default.
Built-in form elements have other features.
For example, with appropriate form elements and the correct inputmode or type,
an on-screen keyboard shows appropriate characters. Using the inputmode attribute on a <div>
cannot do that.
Ensure users are aware of the expected data format
You can define various validation rules for a form control.
For example, say a form field should always have at least eight characters.
You use the minlength attribute, indicating the validation rule to browsers.
How can you ensure users also know about the validation rule? Tell them.
{% Codepen { user: 'web-dot-dev', id: 'b03a4c83688c1b02ab12f9873f1f6614', height: 350, tab: 'html,result' } %}
Add information about the expected format directly beneath the form control.
To make it clear for assistive devices,
use the aria-describedby attribute on the form control,
and an id on the error message with the same value, to connect both.
Help users find the error message for a form control
In a previous module about validation, you learned how to show error messages in case of invalid data entry.
<label for="name">Name</label>
<input type="text" name="name" id="name" required>
For example, if a field has a required attribute, and invalid data is entered, the browser shows
an error message next to the form control when the form is submitted. Screen readers also announce the error message.
You can also define your own error message:
{% Codepen { user: 'web-dot-dev', id: 'b7ed22a0539f9beef4dc03380f51f224', height: 300, tab: 'html,result' } %}
This example needs more changes to connect the error message to the form control.
A simple approach is to use the aria-describedby
attribute on the form control that matches the id on the error message element.
Then, use aria-live="assertive" for the error message.
ARIA live regions announce an error to screen reader users the moment the error is shown.
The problem with this approach for forms with multiple fields,
is that aria-live will usually only announce the first error in the case of multiple errors.
As explained in this article about multiple aria-live announcements on the same action you could create a single message by concatenating all the errors. Another approach would be to announce that there are errors, then announce individual errors when the field is focused.
Ensure users recognize errors
Sometimes designers color the invalid state red,
using the :invalid pseudo-class.
However, to communicate an error or success,
you should never rely only on color.
For people with red-green color blindness,
a green and a red border look almost the same.
It's impossible to see if the message is related to an error.
In addition to color, use an icon, or prefix your error messages with the error type.
<span class="error">
<strong>Error:</strong>Please use at least eight characters.
</span>
Help users to navigate your form
You can change the visual order of form controls with CSS. A disconnect between visual order and keyboard navigation (DOM order) is problematic for screen reader and keyboard users.
Learn more about how to ensure visual order on the page follows DOM order.
Help users to identify the currently focused form control
Use your keyboard to navigate through
this form.
Did you recognize that the styling of the form controls changed once they were active?
This is the default focus style.
You can override it with the
:focus CSS pseudo-class.
Whatever styles you use inside :focus,
always make sure the visual difference between the default state and the focus state is recognizable.
{% Aside %}
If you want to remove the default :focus styles but still show focus indicators for keyboard users,
you can use the :focus-visible CSS pseudo-class.
{% endAside %}
Learn more about designing focus indicators.
Ensure your form is usable
You can identify many common issues by filling out your form with different devices. Use only your keyboard, use a screen reader (such as NVDA on Windows or VoiceOver on Mac), or zoom the page to 200%. Always test your forms on different platforms, especially devices or settings you don't use every day. Do you know someone using a screen reader, or someone using text enlargement software? Ask them to fill out your form. Accessibility reviews are great, testing with real users is even better.
Learn more about doing an accessibility review and how to test with real users.
Resources
Address forms
Filling in an address can be time-consuming and frustrating. What is an Address line 2? You may not have a surname, so what should you enter in a Surname field? Avoid these confusions and help users fill out address forms.
{% Codepen {
user: 'web-dot-dev',
id: '6af9430bd0fc87b3850bb187409ce312',
height: 650
} %}
Ensure your address form is easy to use
Many forms use one field for first name and one for surname.
However, some people don't have a surname, or their names don't have two parts,
so how should they fill in the surname field? Use a single <input> for the name field.
Learn more about handling different name formats.
{% Aside 'caution' %}
Never restrict the allowed characters for names to
Latin-only.
Allow Unicode characters
for the name <input> to ensure everybody can fill in their name.
{% endAside %}
Also use a single <input> for the street address–not every address has a street number.
{% Aside %}
According to research,
Address line 2 can be confusing for users.
Consider using a <textarea> for the whole address,
or hide the Address line 2 field behind a reveal <button>.
{% endAside %}
Be careful with form control descriptions.
For example, users in the US say ZIP, in the UK postcode.
Use <label for="zip">ZIP or postal code (optional)</label>
to make sure users know what data to enter.
Make the postal code field optional–not every address has a postal code.
{% Aside %}
You can use services like
Place Autocomplete or
Loqate to help users look up addresses.
Consider avoiding postcode address lookup,
and only provide address lookup as an extra option: don't force people to use it.
{% endAside %}
Help users enter their address
The autocomplete attribute can help users re-enter their address:
autocomplete="name"autocomplete="street-address"autocomplete="postal-code"autocomplete="country"
{% Aside %}
There are two possible autocomplete values for the country:
country: country or territory codecountry-name: country or territory name
Use autocomplete="country" for <select> where the value for the <option> is the country code.
Want to use an <input> for the country name? Use autocomplete="country-name".
{% endAside %}
You can define multiple values separated by a space for autocomplete.
Say you have a form with a shipping address and another form for a billing address.
To tell the browser which postal code is for the billing address,
you can use autocomplete="billing postal-code".
For the shipping address, use shipping as the first value.
Change the label for the Enter key on on-screen keyboards with the enterkeyhint attribute.
Use enterkeyhint="done" for the last form control, and enterkeyhint="next" for the other form controls.
Resources
Form attributes in depth
HTML element attributes can enhance your <form> and form controls.
Help users fill in form controls
To make it easier for users to fill out forms,
use an appropriate type attribute for your <input> elements.
Browsers display a user interface appropriate for the type,
such as a date picker for an <input> of type date.
Browsers on mobile devices show an adapted on-screen keyboard,
such as a telephone number keypad for type="tel".
Some <input> types also change validation rules for an element when its form is submitted.
<input type="url">, for example, is only valid if it's not empty and the value is a URL.
Ensure users enter data
There are different attributes for providing an appropriate on-screen keyboard on touch devices.
The first option is to use the type attribute, as mentioned above.
Another option is the inputmode attribute supported on
Android and iOS.
In contrast to the type attribute, the inputmode attribute only changes the on-screen keyboard
provided, not the behavior of the element itself. Using inputmode is a good option if you want to
keep the default user interface and the default validation rules of an <input>, but still want an
optimized on-screen keyboard.
{% Aside 'caution' %}
Use type="number" only for incremental fields such as the quantity of a product.
Browsers show an up/down arrow for type="number" which makes no sense for telephone numbers,
payment card or account numbers.
For telephone numbers, use type="tel". For other numbers, use inputmode="numeric" to get a numeric on-screen keyboard.
{% endAside %}
{% Img src="image/tcFciHGuF3MxnTr1y5ue01OGLBn2/bi7J9Z1TLP4IsQLyhbQm.jpg", alt="Two screenshots of Android phones, showing a keyboard appropriate for entering an email addresses (using type=email) and for entering a telephone number (with type=tel).", width="800", height="696" %}
You can change the Enter key on on-screen keyboards with the enterkeyhint attribute.
For example, enterkeyhint="next" or enterkeyhint="done" changes the button label to an appropriate icon.
This helps make it clearer for users what happens when they submit the current form.
{% Img src="image/tcFciHGuF3MxnTr1y5ue01OGLBn2/QoY8Oynpw0CqjPACtCdG.png", alt="Two screenshots of an address form on Android showing how the enterkeyhint input attribute changes the enter key button icon.", width="800", height="696" %}
{% Aside 'warning' %}
Automatically focusing a form control using the autofocus attribute can confuse people,
including screen reader users and people with cognitive impairments. Therefore, our advice is that you should not use the autofocus attribute.
{% endAside %}
Ensure users can submit a form
Say, you fill out a <form>, click the Submit button, but nothing happens.
The issue may be that the button has been disabled with the disabled attribute.
It's a common pattern to disable the Submit button until the form is valid.
In theory this sounds sensible, but you shouldn't disable a Submit button while waiting on complete and valid user input. Instead, highlight invalid data when it's entered, and highlight problematic fields to the user when they submit the form.
However, you may want to disable the Submit button once the form is successfully submitted, but not yet processed. Learn more about disabled buttons.
Help users by showing the data they previously entered
Imagine you have a checkout form with multiple steps.
How do you ensure the previously entered values are still there when the user returns to a previous step?
Use the value attribute to show values already completed.
<label for="name">Name</label>
<input value="Hilda" name="name" id="name" type="text">
There are multiple ways to retrieve the value of a form control in JavaScript.
You can use the
value property, or you can access the value with
getAttribute('value').
There is one big difference,
the value property always returns the current value,
and using getAttribute() always returns the initial value.
Try it out!
Change the text of the name field and watch the console.
Notice how the value property returns the currently visible text,
while getAttribute('value') always returns the initial value.
Learn more about the difference between DOM attributes and DOM properties.
{% Aside %}
To get all form control values use
FormData.
The FormData object is a set of key/value pairs representing the form controls,
and the format is the same as the
submit() method uses.
{% endAside %}
For <input> elements of type checkbox or radio use the checked attribute.
Add it if the user selected an option and remove it otherwise.
Ensure users understand the expected format
The value of the placeholder attribute is a hint for what kind of information is expected.
<label for="name">Name</label>
<input type="text" name="name" id="name" placeholder="Karin">
This may confuse users, as it may seem illogical why a form control appears to be already prefilled. In addition, adding a placeholder can make it difficult to see which form fields still need to be completed. Furthermore, the default style of placeholder text can be hard to read.
In general, be cautious when using the placeholder attribute and never use the placeholder attribute to explain a form control.
Use the <label> element instead.
Learn more about
why you should consider avoiding the placeholder attribute.
A better way to give users a hint about what kind of information is expected is to use an extra HTML element beneath the form control to add an explanation or example.
{% Codepen { user: 'web-dot-dev', id: '90199b1fdcbfbcea425686884344352d', height: 300 } %}
Ensure form controls are ready for validation
There are various HTML attributes available to activate built-in validation.
Use the required attribute to prevent the submission of empty fields.
Additional validations can be enforced with the type attribute.
For example, the value of a required <input> of type="url" must be a URL.
To ensure a user enters a minimum number of characters,
use the minlength attribute.
To disallow any value with more than a maximum number of characters,
use the maxlength attribute.
For numeric input types such as <input type="number">, use the min and max attribute instead.
Find out more about validation: Help users enter the right data in forms.
{% Assessment 'attributes' %}
Resources
Help users avoid re-entering data in forms
After learning about the form element and how to make a form interactive, let's see how you can help users avoid re-entering data.
Make the most of autofill
Filling out forms can be time-consuming. For example, re-entering your address repeatedly on every site where you want to buy something is not a great shopping experience.
Autofill can help you here. You enter your address once. From now on, your browser will offer you the option to fill in the same address for other forms automatically.
You moved to another city? Don't worry about getting the old address as an option forever. You can edit the address data your browser has saved for you to keep it up-to-date.
{% Aside %} Browsers also help with remembering data. You sign up for a site. For the password field, you get the option to generate and store a secure password. If you want to log in later at the same site, your browser offers you the option to fill in the stored password. {% endAside %}
How does autofill work in the browser?
{% BrowserCompat 'html.global_attributes.autocomplete' %}
An address field can look very different on different sites. How does a browser know that it is an address field?
Browsers use
heuristics to identify an address field.
What are the values of the name, type, and id attributes?
Is there an autocomplete attribute present on the form control?
Based on this information, browsers can offer the option to autofill a field with previously entered data of the same type. Browsers can even offer to autofill an entire form.
Help browsers with autofill
Let's see what you can do to help browsers offer the correct autofill options.
Use sensible attribute values
As you learned, browsers can identify the data type by looking at the attributes of a form control.
<label for="email">Email</label>
<input type="email" name="email" id="email">
Do you have a field where users should enter their email address?
Use email as a value for the name, id, and type attribute.
Three hints for the browser that this is an email field.
The autocomplete attribute {: #autocomplete}
There are other examples where it can still be hard for browsers to identify the data type solely from the name, id, and type attributes.
You can help here by using the autocomplete attribute.
{% Codepen { user: 'web-dot-dev', id: '9a2262c52b978968f6ce181f32357f31', height: 300, tab: 'html,result' } %}
Have you entered a name before in the browser you're using? The browser will probably offer you the option to fill it in again for this field in the demo.
You can learn more about using autocomplete and autofill in a later module.
{% Assessment 'auto' %}
Resources
Autofill
Having to re-enter your address for the tenth time is tiring.
Browsers, and you, as a developer, can help users enter data faster, and avoid re-entering data.
This module teaches you how autofill works, and how autocomplete and other element attributes can
ensure that browsers offer appropriate autofill options.
How does autofill work?
In the intro to autofill, you already learned the basics of autofill. But why do browsers offer autofill?
Filling out forms isn't an interesting activity, but still something you do often. Over time, you fill out many forms, and you often fill in the same data. One way to help users fill out forms faster is by offering them the option to automatically fill in form fields with previously entered data. That's autofill.
How do browsers know what data to autofill? Let's have a look at an example form field to find out.
<label for="name">Name</label>
<input name="name" id="name">
If you submit this form field,
browsers store the value (the data you entered) along with the value of the name attribute (name).
Some browsers also look at the id attribute when storing and filling in data.
Say, weeks later, you fill out another form on another website.
This site also contains a form field with name="name".
Your browser can now offer autofill, because a value for name is already stored.
{% Aside %}
Use the :autofill
CSS pseudo-class to style form controls that the browser has autofilled.
Use :autofill and the prefixed version :-webkit-autofill for best browser compatibility.
{% endAside %}
Autofill is especially useful in forms you regularly use, such as sign-up and sign-in, payment, checkout, and forms where you have to enter your name or address.
Let's see how you can help browsers offer the best autofill options by using appropriate values for the autocomplete attribute.
Help users to enter their address
There are many possible values for autocomplete. Let's have a look at addresses first.
{% Codepen { user: 'web-dot-dev', id: 'f6eac7a585a34d0c2de36a365eaa8662', height: 600, tab: 'result,html' } %}
Does your browser already have an address saved for you? Great! After you interact with the first field in the address form, the browser shows you a list of saved addresses. You can choose one, and the browser fills in all fields related to the address. Autofill makes filling out forms fast and easy.
Not every address form has the same fields,
and the order of fields also varies.
Using the correct values for autocomplete ensures that the browser fills in the correct values for a form.
There are values for country, postal-code, and
many more.
{% Aside %}
You can define multiple values separated by a space for autocomplete.
Say, you have a form with a shipping address and another form for a billing address.
To tell the browser which is the postal code for the billing address,
you can use autocomplete="billing postal-code".
For the shipping address, use shipping as the first value.
{% endAside %}
Ensure users can sign in fast and use secure passwords
Many people aren't good at remembering passwords. The most common password is '123456', followed by other easy-to-remember combinations. How can you use secure and unique passwords without remembering them all?
Browsers have built-in passwords managers to generate, save, and fill in passwords for you. Let's see how you can help browsers with autofilling emails and managing passwords.
{% Codepen { user: 'web-dot-dev', id: 'e7e95d2d671d577b0a141aaab4388b28', height: 380 } %}
You can use autocomplete="email" for an email field,
so users get the autofill option for an email address.
As this is a sign-up form, users shouldn't get the option to fill in previously used passwords.
You can use autocomplete="new-password" to ensure browsers offer the option to generate a new password.
{% Aside %} To ensure a secure sign-up form it may be better to use a third-party identity provider, instead of building your own authentication system. {% endAside %}
On the sign-in form, you can use autocomplete="current-password"
to tell browsers to offer the option to fill in previously saved passwords for this website.
You can set up two-factor authentication on many websites. In addition to the password, a one-time code is sent via SMS or a two-factor authentication app.
Wouldn't it be great if the code you received in the SMS message was suggested by the on-screen keyboard,
and you could directly select it to fill in the value? On Safari 14 or later, you can use
autocomplete="one-time-code" to achieve this.
On Chrome on Android, you can use the
WebOTP API to achieve this with JavaScript.
Learn more about how to verify phone numbers on the web with the SMS OTP form best practices.
{% Aside 'caution' %} SMS isn't the most secure method of authentication by itself, because phone numbers can be recycled and hijacked. Consider using other two-factor authentication methods or multifactor authentication.
Learn more about multifactor authentication. {% endAside %}
Help users fill in their credit card information
On many e-commerce websites, you can use your credit card to purchase products. Sites may use third-party payment platforms that provide their own forms, but if you do need to build your own payment forms, make sure people can easily fill in payment information.
You can use the autocomplete attribute again,
to ensure browsers offer the correct autofill options.
There are values for the credit card number cc-number, credit card expiration date cc-exp,
and all other information needed for a credit card payment.
Use a single input for numbers
such as credit card numbers and telephone numbers,
to ensure browsers offer autofill.
Use standard form elements, for example,
a <select> for the payment card dates,
instead of custom elements, to ensure autocomplete is available.
Learn more about helping users to avoid re-entering payment data.
Ensure autofill works for all your fields
In addition to addresses, account information, and credit card information, there are many more fields where browsers can help users with autofill.
When adding a telephone field to your form check if you can use any of the available values for autocomplete. Found an appropriate value for your form field? Add it.
Using suitable values for the autocomplete attribute helps browsers offer the best autofill option,
and helps users fill out forms faster.
Help browsers understand that a field shouldn't be autofilled
You learned how autofill works, how you can help browsers with autofill, and why autofill makes it convenient for users to fill out forms. Sometimes, though, you don't want browsers to offer autofill. Let's have a look.
<label for="one-time-code">One-time code</label>
<input autocomplete="off" type="text" name="one-time-code" id="one-time-code">
One place where autofill isn't helpful is when entering one-off,
unique values such as a one-time code field.
The value is different every time,
and the browser shouldn't save values or offer an autofill option.
You can use autocomplete="off" for such fields to prevent autofill.
{% Aside %} Autofill and autocomplete provide accessibility benefits. Using autofill, people don't have to remember information or manually re-enter data.
For more information, see the W3C accessibility guidelines {% endAside %}
Another use case for autocomplete="off" is a honeypot field (see previous module).
Even though the field isn't visible, browsers may autofill it with the rest of the fields.
Turning autofill off ensures a real user isn't identified as a bot,
due to the field being completed automatically.
{% Aside %}
You learned how important it is to use unique, and secure passwords.
In-browser password managers ensure that you use strong passwords.
Therefore browsers will still offer autofill options for passwords, even though autocomplete="off" is used,
to allow these password managers to do their job.
Learn more about the autocomplete attribute and sign-in fields. {% endAside %}
You should only disable autofill if you are sure it will help users.
{% Assessment 'autofill' %}
Resources
Conclusion and next steps
Congratulations! You've made it to the end. So far, you've learned a lot about forms, and hopefully you feel more comfortable now building your own forms.
What's next? One of the best ways to learn is to try out what you've learned: implement an HTML form of your own. To continue to build your HTML forms knowledge, here are some additional resources to explore:
Additional resources
- HTML forms guide on MDN
- Learn CSS
- WebAIM: Creating Accessible Forms
- Payment and address forms best practices
- Sign-up form best practices
- Sign-in form best practices
- Best Practices For Mobile Form Design
- OWASP Cheat Sheet series: best practices for privacy and security.
- NIST Identity Guidelines: US National Institute of Standards and Technology guidelines for passwords and other identity topics.
- 12 best practices for user account, authorization and password management: outlines core back-end principles for managing user accounts.
- Passwords Evolved: Authentication Guidance for the Modern Era: detailed password guidance from Troy Hunt, creator of haveibeenpwned.com,
- We Didn't Encrypt Your Password, We Hashed It. Here's What That Means: also from Troy Hunt — here he explains the difference between encryption and hashing.
Brought to you by
{% include 'partials/authors-stack.njk' %}
Test forms across devices and platforms
You can fill out a form in many ways. People might use your form on their smartphone while standing in a crowded bus, or with a screen reader, or from an old laptop. Let's see how you can ensure your form works on different devices, and in different contexts.
Ensure your form works for keyboard users
A good first test to ensure your form is accessible is to fill out your form using only your keyboard.
Open your form and try to navigate with the tab key.
Is it clear what form field is currently active?
To help users understand which form field is active,
you can use focus indicators.
{% Codepen { user: 'web-dot-dev', id: 'be0759d9436d84426a795a9ae399eff9', height: 300 } %}
Try it out.
Use your tab key to navigate to the input.
Do you see the outline when the input is active?
That's the focus indicator.
You can add focus indicators using the
:focus CSS pseudo-class.
input:focus {
outline: 4px solid #222;
}
Learn more about designing accessible focus indicators.
{% Aside %}
If you want to remove the default :focus styles but still show focus indicators for keyboard users,
you can use the :focus-visible CSS pseudo-class.
Learn more about
:focus-visible.
{% endAside %}
Help users navigate your form
Another good test of usability and accessibility is to ensure that the logical tab order follows the visual tab order. How can you test the tab order? Tab completely through your form. Do you notice any illogical navigation jumps? Make sure the DOM order matches the visual order.
Learn more about how to ensure visual order on the page follows DOM order.
Help users complete your form on touch devices
Great! You made sure your form works with the keyboard. Now, let's see how you can ensure it also works on touch devices such as mobile phones.
Open your form on your touch device,
fill in all fields and submit the form.
Did you have to tap multiple times to select a form control?
The issue may be that the tap areas are too small.
Make sure the
tap target size of a button is at least 48px,
and that each <input> and <select> is large enough to tap into.
You can also help users navigate your form on touch devices by adding enough spacing between form controls.
Ensure users get an optimized keyboard
In previous modules, you learned how to activate a different on-screen keyboard
using the type or inputmode attributes.
{% Codepen { user: 'web-dot-dev', id: '88f1ffed62ef6bdd513351823b7aaa0f', height: 300, tab: 'html,result' } %}
Open
the demo
on your phone and tap in the telephone number field.
Notice that numbers are shown by default on the on-screen keyboard,
along with other characters you may need for a telephone number.
Use type="tel" to get an on-screen keyboard optimized for entering telephone numbers.
{% Img src="image/ZDZVuXt6QqfXtxkpXcPGfnygYjd2/nX50IOaBUCP8mcHqr3CT.jpg", alt="Two screenshots of an input element with type='tel' on iOS and Android, showing how the type attribute changes the on-screen keyboard.", width="800", height="696" %}
Use a phone to try it out for yourself,
and see if you can easily enter every character needed to fill in a telephone number.
If you're curious how the on-screen keyboard works for another type, try type="email" in the demo.
Make sure form buttons aren't hidden
Imagine that you filled out a long form,
and you're ready to submit it. But where is the Submit button?
It may be behind the browser toolbar, located at the bottom of the screen.
One way to ensure that buttons are visible is to use the env() CSS function.
Learn how to
ensure buttons aren't obscured by device user interfaces.
Ensure your form works on different platforms
Try to test your forms on as many devices as possible. Have an old laptop? Open the default browser on it and test your form. Your friend has a tablet? Borrow it and test your form there as well.
Does some styling look different in one browser? You can learn how to ensure your styles work across platforms in a later module.
BrowserStack offers free test accounts for open source projects, and Browserling offers a free trial to test on different browsers, devices and operating systems.
Help users complete your forms in different contexts
People don't just use different browsers, devices and operating systems. People also use your forms in different contexts. Try it out! Is the sun shining outside right now? Take your phone and go outside. Using your form in bright light is a good way to test if contrast ratios are usable.
Learn more about color and contrast accessibility.
Ensure your form works with poor connectivity
Say you travel somewhere on a train. You open a web page on your phone. You wonder how loading a website can take so long 😕.
You can simulate slow connections and different network types with WebPageTest or DevTools.
Learn more about testing with low bandwidth and high latency.
Help users use your form when they are on the go
Imagine that you're walking to an appointment. Suddenly, your phone rings, you answer the call, and at the same time, get an alert from your insurance company to complete the application form you started. You open the form, trying to fill it out while still walking and talking.
Remember that people will use your forms in many different contexts. In stressful situations, while doing other things, while on the go. You can help users by ensuring your form is easy to use.
Try setting yourself a time limit to complete your form. Try to simulate imperfect conditions where you can test your form.
Make sure to share test results
Document all tests, and share the results with your team. This will help prioritize action items, to ensure everybody in your team is aware of the most important tasks.
Learn more about sharing test results.
{% Assessment 'cross-platform-testing' %}
Resources
- WebPageTest: Website Performance and Optimization Tests
- Testing with low bandwidth and high latency
- Sharing test results
Gathering data
In this module, you learn how to define goals, analyze your forms, measure changes, and get notified about new issues.
Ensure you identify issues, problems, and goals
As a first step, you need a way to identify issues and goals. One way is to use analytics to get an overview of where your form may need improvements.
After analytics is up and running for your site, you can monitor bounce rate and other metrics for every page with a form within your site. If the bounce rate is high, users may be leaving your site without submitting the form. There are many potential reasons why this happens: for example, the form may be too complicated, or it's unclear what data should be entered.
{% Aside %} Always respect the privacy of your users, with analytics data as well as form data. Collect the minimum amount of data you require and be transparent with your users about what data you are collecting. There are also privacy and data protection laws that you must follow. In some countries, you need consent from a user before you can collect data. {% endAside %}
To get a clearer picture of a high bounce rate, you can define goals to identify in detail where users leave your form.
A goal funnel (or conversion funnel) is a series of interactions that lead to a goal such as a completed purchase (a conversion). Each goal funnel consists of individual steps such as opening a shopping cart page or proceeding to a checkout page.
The goal funnel for your form might look like this:
- The user opens page A with the form.
- The user fills out the name field.
- The user fills out the postal code field.
- The user submits the form.
- The user navigates to page B.
Goal funnels can give you an overview of where users leave your form, and where you need to improve it.
As well as using analytics to track page metrics such as time-on-page or exit rates,
you can also monitor events.
These allow you to track individual interactions,
such as button clicks or interactions with form fields.
For example, you can set up a custom event,
which fires when a user fills out a specific <input>.
Or you can track the percentage of your users who submit a form.
Analyze your forms
You can create custom events for almost every interaction, and monitor form completion step-by-step. This isn't always necessary though: it's better to start with a small number of custom events, and only add more if you need more details to find an issue with your form.
Focus first on the most important goals, such as successful checkout. You can always expand your goals later if more details are needed. Identify your main goals, measure achievement of these goals, analyze the data, identify potential changes, adapt your form and measure again.
Ensure that you fix problems with your form
Say you identified an issue with your form. What should you do next? First, you need to correct the issue and deploy a new version of your form. After some days, it's now time to measure how effective the change was.
Did the bounce rate decrease for your form? Great, now look at the goal funnel to see which parts improved. Are a high proportion of users still leaving before they fill in the postal code, for example? Adapt your goal funnel steps if necessary, deploy further changes, and analyze your form again until you are happy with the outcome.
Get notified when there are problems
How can you get notified about potential issues without constantly checking your analytics dashboard? You can set up alerts. A common alert is about traffic drops. You can monitor your form pages, and get notified when the traffic is unusually low for a day. Or, coming back to bounce rates, you can set up an alert if the bounce rate for a page increases significantly.
Alerts help you identify issues fast, and ensure you don't miss new problems with your form.
Want to get a summary of your site's analytics? You can create reports, and get notified via email. This is a good way to monitor form usage.
Resources
Design basics
In the first section, you learned how to build a basic form. This section is all about best practices. In this module, learn about user experience (UX), and why a well-implemented user interface (UI) can make a big difference.
Creating user-friendly interfaces
Filling out forms can be time-consuming and frustrating. It doesn't have to be. To guarantee a great UX, make sure the UI is intuitive. Ensure you deliver optimal form structure and graphic design (layout, spacing, font size and color), and logical UI (such as label wording and appropriate input types). Let's have a look at how you can improve your form and make it easy to use.
Labels
Do you remember what the <label> element is for?
A label describes a form control.
A visible and well-written label helps the user understand the purpose of a form control.
Use a label for every form control
Do you want to add a new form control to your form? Start by adding the label for the new field. This way, you don't forget to add it.
Adding labels first also helps you to focus on the form's goals–what data do I need here? Once this is clear, you can focus on helping the user enter data and complete the form.
{% Aside 'caution' %}
Never misuse the placeholder attribute as a label.
The placeholder is meant for giving a hint about the type of data you should enter,
not for describing a form control.
Learn more about why you should consider avoiding placeholders. {% endAside %}
Label wording
Say that you want to describe an email field. You could do so as follows:
<label for="email">Enter your email address</label>
Or you could describe it like this:
<label for="email">Email address</label>
Which description do you choose?
For our example, the wording 'Email address' is preferred, since short labels are easier to scan, reduce visual clutter, and help users to understand what data is needed faster.
Label position
With CSS, you can position a label on the top, bottom, left, and right of a form control. Where do you place it?
Research shows that best practice is to position the label above the form control, so the user can scan a form quickly and see which label belongs to which form control.
Designing forms
Good form design can
significantly reduce form abandonment rates.
Help users enter data by using the appropriate element and input type
There are various
form elements and input types you can choose from.
To offer the best UX, use the most suitable element and input type for your use case.
If the user should fill in multiple lines of text, use the <textarea> element.
Where they need to accept your site's terms and conditions, use <input type="checkbox">.
{% Aside %}
Depending on the type or inputmode attribute (more about this
later)
a different on-screen keyboard is shown on touch devices.
If you use type="tel", users on touch devices get a keyboard showing only the characters needed to enter a telephone number.
By using type="email" the mobile device displays a keyboard optimized for entering an email address.
{% endAside %}
You should also visually differentiate between different types of form controls. A button should look like a button. An input like an input. Make it easy for users to recognize the purpose of a form control. For example, If something looks like a link, clicking on it should open a new page, and not submit a form.
{% Codepen { user: 'web-dot-dev', id: 'c369bda56ae3bc88aa0116614f79e40d', height: 400 } %}
Help users navigate forms
An extravagant layout can be fun, but may get in the way of completing a form.
In particular, research shows that it's best to use only a single column. Users don't want to spend time searching where the next form control is. By using one column, there is only one direction to follow.
{% Aside %} Always ensure that forms are accessible for people who use only the keyboard to navigate websites—either through preference or circumstance. Test your form by only using keyboard navigation. The tab order should follow the visual order. You can view the source order in Chrome DevTools. {% endAside %}
Help users interact with forms
To avoid accidental taps and clicks,
and help users interact with your form, make your buttons big enough.
The recommended
tap target size of a button is at least 48px.
You should also add enough spacing between form controls using the margin
CSS property to help avoid accidental interactions.
The default size of form controls is too small to be really usable. You should increase the size by using padding and changing the default font-size.
{% Aside %}
Use at least 1rem (which for most browsers has a default value of 16px)
as the font-size for your form controls.
This prevents a page zoom
on iOS Safari when a form control is focused.
{% endAside %}
You can define different sizes for different pointing devices,
for example, a mouse, using the pointer CSS media feature.
// pointer device, for example, a mouse
@media (pointer: fine) {
input[type="checkbox"] {
width: 15px;
height: 15px;
}
}
// pointer device of limited accuracy, for example, a touch-based device
@media (pointer: coarse) {
input[type="checkbox"] {
width: 30px;
height: 30px;
}
}
Learn more about the
pointer CSS media feature.
Show errors where they happen
To make it straightforward for users to find out which form control needs their attention, display error messages next to the form control they refer to. When displaying errors on form submission, make sure to navigate to the first invalid form control.
Make it clear to users what data to enter
Make sure users understand how to enter valid data. Do they need to enter at least eight characters for a password? Tell them.
<label for="password">Password (required)</label>
<input required minlength="8" type="password" id="password" name="password" aria-describedby="password-minlength">
<span id="password-minlength">Enter at least eight characters</span>
Make it clear to users which fields are required
<label for="name">Name (required)</label>
<input name="name" id="name" required>
If a field is mandatory, make it obvious! The Anatomy of Accessible Forms explains alternatives for indicating required fields. If most fields in a form are required, it may be better to indicate optional fields.
How can you connect error messages to form controls to make them accessible for screen readers? You can learn about this in the next module.
{% Assessment 'design-basics' %}
Resources
- Create Amazing Forms
- Best Practices For Mobile Form Design
- Baymard Institute: E-Commerce UX Research
- Don't Make Me Think
Form fields in depth
To provide the best possible user experience,
make sure to use the element and element type that's most appropriate for the data the user is entering.
Help users fill in text
To provide users with a form field for inserting text, use the <input> element.
It's the best choice for single words and short texts.
For longer text, use the <textarea> element.
This allows multiple lines of text,
and makes it easier for the user to see the text they entered, as the element is scrollable and resizable.
{% Aside %}
A <textarea> is resizable by default. You can disable the resizing with CSS and the resize property: resize: none.
However, people may want to see the text they entered all at once.
At least keep the option to resize an <textarea> vertically.
Use resize: vertical to ensure users can resize vertically but not horizontally.
Learn more about why disabling resize on <textarea> is bad for UX.
{% endAside %}
Ensure users enter data in the correct format
Do you want to help users fill in a telephone number?
Change the type attribute to type="tel" for the <input>.
Users on mobile devices get an adapted on-screen keyboard,
ensuring they can enter the telephone number faster and more easily.
For an email address, use type="email".
Again, an adapted on-screen keyboard is shown.
Use the required attribute to make the form field mandatory.
When the form is submitted, the browser checks that the input has a value and that it's valid: in this case,
that it's a well-formatted email address.
Learn more about the different input types. These also provide built-in validation features.
Help users fill in dates
When do you want to start your next trip?
To help users fill in dates, use type="date".
Some browsers show the format as a placeholder such as yyyy-mm-dd,
demonstrating how to enter the date.
All modern browsers provide custom interfaces for selecting dates in the form of a date picker.
{% Codepen { user: 'web-dot-dev', id: '93693a7c2ce3d39ec5aa9088159b0601', height: 300 } %}
Help users select an option
To ensure users can select or unselect one possible option, use type="checkbox".
Do you want to offer multiple options?
Depending on your use case, there are various alternatives.
First, let's look at possible solutions if users should only be able to choose a single option.
You can use multiple <input> elements with type="radio" and the same name value. Users see all options at once, but can only choose one.
{% Codepen { user: 'web-dot-dev', id: '617e51cdae8adeca641d289da1a3e8a2', height: 300 } %}
Another option is to use the <select> element.
Users can scroll through a list of available options and choose one.
{% Codepen { user: 'web-dot-dev', id: '7225931596b7eeafc448ad55d9c980fd', height: 300 } %}
For some use cases, such as choosing a range of numbers,
<input> of type range may be a good option.
{% Codepen { user: 'web-dot-dev', id: '242b1ec7ea180c4995711ceabd749c59', height: 300 } %}
{% Aside %}
You can use the accent-color CSS property to change the color of form controls,
including <input type="range">.
{% BrowserCompat 'css.properties.accent-color' %}
{% endAside %}
Do you need to offer the ability to select multiple options?
Use a <select> element with the multiple attribute or multiple <input> elements of type checkbox.
You may also use an <input> in combination with the <datalist> element.
This gives you a combination of a text field and a list of <option> elements.
{% Aside 'codelab' %}
How would you tackle the selection of multiple items?
Try to implement different versions.
Use a <select> element, <datalist> element,
multi-select custom element, or build something completely different.
{% endAside %}
Ensure users can fill in different types of data
There are more input types for specific use cases.
There is an <input> of type color to provide users with a color picker in supported browsers,
and there are various other types as well. To ensure users can enter their password, use <input>
with type="password". Every character entered is obscured by an asterisk ("*") or a dot ("•"),
to ensure the password can't be read.
Do you want to include a unique security token in the form data?
Use <input> with type="hidden".
The value of an <input> of type hidden can't be seen or modified by users.
To enable users to upload and submit files, use <input> with type="file".
{% Codepen { user: 'web-dot-dev', id: '74f2d88b9185b8fa880ea5a92f48f690', height: 300 } %}
{% Aside %}
An <input> of type file can also allow multiple uploads at once.
You can also specify which file formats are allowed.
Learn more about <input type="file">.
{% endAside %}
You can also define custom elements if you have a special use case, where no built-in element or type is suitable.
Help users fill out your form
There are many form elements and types, but which one should you choose?
For some use cases, it's straightforward to choose the appropriate element and type,
such as <input type="date">. For others, it depends.
For example, you can use multiple <input> elements with type="checkbox" or a <select> element.
Learn more about choosing between listboxes and dropdown lists.
In general, make sure to test your form with real users to find the best form element and type.
{% Assessment 'fields' %}
Resources
The form element in depth
In a previous module, you learned how to use the <form> element.
In this module, you learn how a form works, and when to use a form.
Should you use a <form> element?
{% BrowserCompat 'html.elements.input' %}
You don't always need to put form controls in a <form> element.
For example, you might provide a <select> element for users to choose a product category.
You don't need a <form> element here, as you're not storing or processing data on your backend.
However, in most cases when you store or process data from users,
you should use the <form> element.
As you will learn in this module, using a <form> gives you a lot of built-in functionality from browsers that you would otherwise have to build yourself.
A <form> also has many accessibility features built-in by default.
If you want to avoid a page reload when a user submits a form,
you can still use the <form> element, but enhance it with
JavaScript.
{% Aside %}
It's possible to replicate form functionality with JavaScript, but a purely script-based approach is
unlikely to be as robust, accessible and future-proof as a built-in <form>.
Find out why not everybody has JavaScript. {% endAside %}
How does a <form> work?
You learned that a <form> is the best way to handle user data.
You may wonder now, how does a form work?
The <form> is a container for interactive form controls.
<form method="post">
<label for="name">Name</label>
<input type="text" name="name" id="name">
<button formaction="/name">Save</button>
</form>
How does form submission work?
When you submit a <form>, the browser checks the <form> attributes. The data is sent as a GET
or POST request according to the method attribute. If no method attribute is present, a
GET request is made to the URL of the current page.
How can you access and process the form data?
The submitted data can be handled by JavaScript on the frontend using a GET request,
or by code on the backend using a GET or POST request.
Learn more about
request types and transferring form data.
{% Aside %}
The Submit button may have a formmethod attribute.
If this is the case, the value defined there is used.
{% endAside %}
When the form is submitted, the browser makes a request to the URL that is the value of the action attribute.
In addition, the browser checks if the Submit button has a formaction attribute.
If this is the case, the URL defined there is used.
Furthermore, the browser looks up additional attributes on the <form> element,
for example, to decide if the form should be validated (novalidate),
autocomplete should be used (autocomplete="off") or what encoding should be used (accept-charset).
Try to build a form where users can submit their favorite color.
The data should be sent as a POST request, and the URL where the data will be processed should be /color.
{% Details %} {% DetailsSummary 'h3' %} Show form {% endDetailsSummary %} One possible solution is using this form:
<form method="post" action="/color">
<label for="color">What is your favorite color?</label>
<input type="text" name="color" id="color">
<button>Save</button>
</form>
{% endDetails %}
Ensure users can submit your form
There are two ways to submit a form.
You can click the Submit button, or press Enter while using any form control.
{% Aside %} Use an actionable name for your Submit button, for example, 'Proceed to Payment' or 'Save Changes', rather than just 'Submit'.
Don't disable the Submit button while still awaiting valid user input, but consider disabling it once the form has been submitted, preventing multiple requests to your server.
Learn more about Submit button best practices. {% endAside %}
You can add a Submit button in various ways.
One option is to use a <button> element inside your form.
As long as you don't use type="button" it works as a Submit button.
Another option is to use <input type="submit" value="Submit">.
{% Aside %}
You can use the enterkeyhint
attribute to change the label of the Enter key for on-screen keyboards. This helps make it clearer
for users what happens when they submit the current form.
{% endAside %}
A third option is to use <input type="image" alt="Submit" src="submit.png">,
to create a graphical Submit button.
However, now that you can create graphical buttons with CSS, it's not recommended to use type="image".
{% Aside %} Always give users as much time as they want to submit a form. Defining timeouts on your form will cause data loss, and frustrates your users.
For more information, see W3C form timeout guidelines. {% endAside %}
Enable users to submit files
Use <input type="file"> to enable people to upload and submit files if necessary.
<label for="file">Choose file to upload</label>
<input type="file" id="file" name="file" multiple>
First, add an <input> element with type="file" to your form.
Add the multiple attribute if users should be able to upload multiple files.
{% Codepen { user: 'web-dot-dev', id: '6a600920c65a81e55576c6b52b4e5efa', height: 300, tab: 'html,result' } %}
One more change is needed to ensure users can upload files—set the enctype attribute on your form.
The default value is application/x-www-form-urlencoded.
<form method="post" enctype="multipart/form-data">
…
</form>
To make sure files can be submitted, change it to multipart/form-data.
Without this encoding, files can't be sent with a POST request.
{% Assessment 'form' %}
Resources
Use forms to get data from users
Imagine you want to ask people on your website about their favorite animal. As a first step, you need a way to collect the data.
How do you do this with HTML?
{% Glitch 'learn-forms-get-started' %}
In HTML, you can build this using the form element (<form>),
an <input> with a <label>, and a submit <button>.
{% Aside %} You may wonder where the styles for this example are coming from. They are coming from a general stylesheet included in all demos.
Interested in styling forms? You can learn about this in a later module. {% endAside %}
What is a form element?
<form>
<label for="animal">What is your favorite animal?</label>
<input type="text" id="animal" name="animal">
<button>Save</button>
</form>
The form element consists of the start tag <form>,
optional attributes defined in the start tag, and an end tag </form>.
Between the start and end tag, you can include form elements like <input> and <textarea>
for different types of user input.
You will learn more about form elements in the next module.
{% Aside %} Use HTTPS to protect all websites and forms, not only if you handle sensitive data. This way, all data is encrypted.
Find out more: Secure connections with HTTPS. {% endAside %}
Where is the data processed?
When a form is submitted (for example, when the user clicks the Submit button), the browser makes a request. A script can respond to that request and process the data.
{% Aside %} A script (running on the server or the client) is needed to process the form. It may validate the data, save it into a database, or do other operations based on the form data. {% endAside %}
By default, the request is made to the page where the form is shown.
Say you want a script running at https://web.dev
to process the form data—how would you do that?
Try it out!
{% Details %}
{% DetailsSummary 'h3' %} Toggle answer {% endDetailsSummary %}
You can select the location of the script by using the action attribute.
<form action="https://example.com/animals">
...
</form>
{% endDetails %}
The preceding example makes a request to https://example.com/animals.
A script on the example.com backend can handle requests to /animals
and process data from the form.
How is the data transferred?
By default, form data is sent as a GET request,
with the submitted data appended to the URL.
If a user submits 'frog' in the example above, the browser makes a request to the following URL:
https://example.com/animals?animal=frog
In this case, you can access the data on the frontend or the backend by getting the data from the URL.
If you want, you can instruct the form to use a POST request by changing the method attribute.
<form method="post">
...
</form>
Using POST, the data is included in the
body of the request.
The data will not be visible in the URL and can only be accessed from a frontend or backend script.
What method should you use?
There are use cases for both methods.
For forms that process sensitive data use the POST method.
The data is encrypted (if you use HTTPS) and only accessible by the backend script that processes the request.
The data is not visible in the URL. A common example is a sign-in form.
Now that you've learned about the form element itself, it's time to learn about form fields to make your forms interactive.
{% Assessment 'form-element' %}
Resources
Help users enter data in forms
To make a form interactive, you need to add form elements. There are controls to enter and select data, elements that describe controls, elements that group fields, and buttons to submit a form.
What are form elements?
{% Codepen { user: 'web-dot-dev', id: 'a4a252d5c8511f94c0f56cabd2e73178', height: 400, tab: 'html,result' } %}
You see two <input> elements, <input type="text"> and <input type="file">. Why do they look different?
Based on the element name and the type attribute, browsers show different user interfaces, use different validation rules, and provide many other features. Using the appropriate form control helps you build better forms.
{% Aside %} The default styles of form controls differ between browsers, operating systems, and platforms. You will learn in a later module how to style forms cross-platform. {% endAside %}
Labels for form elements
Say you want to add an input where the user can enter their favorite color.
For this, you need to add an <input> element to your form.
But, how does the user know that they should fill in their favorite color?
To describe form controls, use a <label> for each form control.
<label for="color">What is your favorite color?</label>
<input type="text" id="color" name="color">
The for attribute on the label element matches the id attribute on the input.
{% Aside %}
Describe every form control with a <label> rather than using some other HTML element.
This makes the form control accessible to screen readers, and provides a bigger target, since you
can tap or click the label to set focus on the control.
Click the label in our demo to try this out. {% endAside %}
Capturing user input {: #input}
As the name suggests, the <input> element is used to gather input from users.
<label for="color">What is your favorite color</label>
<input type="text" id="color" name="color">
As mentioned before, the id attribute connects the <input> to the <label>.
What about the name and type attribute in this example?
The name attribute
Use the name attribute to identify the data the user enters with the control.
If you submit the form, this name is included in the request.
Say that you named a form control mountain and the user entered Gutenberg,
the request includes this information as mountain=Gutenberg.
Try to change
the name of the form control to hill.
If you do this correctly, and submit the form, hill is visible in the URL.
The input type {: #type}
There are different
types of form controls, all
with useful built-in features that work across different browsers and platforms. Based on the type
attribute, the browser renders different user interfaces, shows different on-screen keyboards, uses
different validation rules, and more. Let's see how to change the type.
{% Codepen { user: 'web-dot-dev', id: '6a3705c993516bb294dbcddb084f0ba4', height: 300 } %}
By using type="checkbox" the browser now renders a checkbox instead of a text field.
The checkbox also comes with additional attributes.
You can set the checked attribute, to show it as checked.
{% Aside %}
The default value for type is text. This means that if you simply want a text <input>, you can
leave out the type attribute.
{% endAside %}
There are various other types you can choose. We have a detailed look in a later module.
Allow multiple lines of text
Say, you need a field where a user can write a comment.
For this, wouldn't it be great if they can enter multiple lines of text?
This is the purpose of the <textarea> element.
<label for="comment">Comment</label>
<textarea id="comment" name="comment"></textarea>
Pick from a list of options
How do you give users a list of options to select from?
You can use a <select> element to achieve this.
{% Aside %}
If you want to enable text input with additional predefined options,
you can also use an <input> element together with the
datalist element.
You can try out datalist examples here.
{% endAside %}
<label for="color">Color</label>
<select id="color" name="color">
<option value="orange">Orange</option>
<option value="pink">Pink</option>
</select>
First, you add a <select> element.
As with all other form controls, you connect it to a <label> with the id attribute
and give it a unique name using the name attribute.
In between the start and end tag of the <select> element,
you can add multiple <option> elements, each representing one selection.
Each option has a unique value attribute, so you can tell them apart when processing the form data.
The text inside the option element is the human-readable value.
If you submit the form using this <select> without changing the selection,
the request will include color=orange. But how does the browser know which option should be used?
The browser uses the first option in the list, unless:
- One
<option>element has theselectedattribute. - The user chooses another option.
{% Codepen { user: 'web-dot-dev', id: '7225931596b7eeafc448ad55d9c980fd', height: 300 } %}
Pre-select an option
With the selected attribute you can pre-select one option.
This becomes the default, regardless of the order in which <option> elements are defined.
{% Codepen { user: 'web-dot-dev', id: '5324a9e3d1c3c695932ee95378b63436', height: 300, tab: 'html,result' } %}
Grouping form controls
Sometimes you need to group form controls. You can use the <fieldset> element to do that.
<fieldset>
<legend>What is your favorite web technology</legend>
<label for="html">HTML</label>
<input type="radio" name="webfeature" value="html" id="html">
<label for="css">CSS</label>
<input type="radio" name="webfeature" value="css" id="css">
</fieldset>
Did you notice the <legend> element inside the <fieldset> element? What do you think it is used for?
If your answer is "to describe the group of form controls", you're right!
Every <fieldset> element requires a <legend> element,
just as every form control needs an associated <label> element.
The <legend> also has to be the very first element in the <fieldset>.
After the <legend> element, you can define the form controls which should be part of the group.
Submitting a form
After learning how to add form controls, and group them, you may wonder how a user can submit a form?
The first option is to use a <button> element.
<button>Submit</button>
After a user clicks the Submit button,
the browser makes a request to the URL specified in the <form> element's
action attribute
with all data from the form controls.
{% Aside 'warning' %}
Every <button> element inside a form works as a Submit button by default.
Sometimes you don't want this: for example,
when using a <button> to toggle visibility for a password field.
To disable the default Submit behavior, you can add type="button" to the <button>.
This tells the browser that the <button> should not submit the form.
{% endAside %}
You can also use an <input> element with type="submit" instead of a <button> element.
The input looks and behaves just like a <button>. Instead of using a <label> element to describe
the <input>, use the value attribute instead.
<input type="submit" value="Submit">
In addition, a form can also be submitted by using the Enter key when a form field has focus.
{% Assessment 'form-fields' %}
Resources
- The label element
- The input element
- The textarea element
- The select element
- The fieldset element
- The button element
Identity
{% Aside 'warning' %}
User account authentication
is a complex issue in terms of privacy and security.
To guarantee the safety of user accounts, it may be better to use a
third-party identity provider,
instead of building your own secure authentication system.
Learn more about
best practices for account authentication, and password management.
{% endAside %}
Help users sign up
The sign-up form is often the first interaction with a form on your website. Good sign-up form design is critical, and a secure form is essential.
{% Aside %}
Only require sign-up if you really need to.
If you only want to store information between navigations,
consider using client-side storage.
For checkout forms, add a guest checkout by default.
You can ask new customers if they want to sign up after they've completed a purchase,
or when they enter address details.
{% endAside %}
Let's look at a sign-up form, and how you can help users sign up on your website.
{% Codepen {
user: 'web-dot-dev',
id: '42a33b97484873a214f8a945a3b55f71',
height: 450
} %}
Keep the sign-up form minimal and only show the required form controls to create an account. Don't double up your form controls to help users get their account details right. Send a confirmation email instead.
Help users fill in their account details
You can help users fill in their account details by using the appropriate autocomplete attribute.
Use autocomplete="email" for the email field, and autocomplete="new-password" for a new-password field.
Learn more about autofilling input controls.
{% Aside 'warning' %}
Don't disallow pasting into password fields.
It's annoying for users, encourages passwords that are memorable and less secure,
and makes it harder for password managers to suggest and autofill a secure password.
Learn more about why you should always
allow pasting passwords.
{% endAside %}
You can also help users enter a secure password by offering a reveal-password <button>.
Learn more about the reveal-password pattern.
Ensure your sign-up form is secure
Never store or transmit passwords in plain text. Make sure to salt and hash passwords—and don't try to invent your own hashing algorithm.
Offer multi-factor authentication, especially if you store personal or sensitive data. SMS OTP best practices and Enabling Strong Authentication with WebAuthn explain how to implement multi-factor authentication.
Ensure users don't use compromised passwords. For example, use the API from Have I Been Pwned to detect compromised passwords, and suggest your users fill in a different new password, or warn them if their password becomes compromised.
Help users sign in
Let's see how to build a sign-in form to ensure users can easily sign in to your website.
{% Codepen {
user: 'web-dot-dev',
id: '89610184f6c51990ab9484b593e1d951',
height: 500
} %}
Make the location of sign-up and sign-in buttons obvious. Ensure your form is usable on touch devices:
- The tap target size of buttons is at least 48px.
- The
font-sizeof your form elements is big enough (20pxis about right on mobile). - There is enough space (
margin) between form controls, and that inputs are large enough (use at leastpadding: 15pxon mobile).
Help users fill in their email and password
Help browsers and password managers autofill account details.
Use autocomplete="email" for the email field,
and autocomplete="current-password" for a current password field.
To help users manually fill in their account details, use type="email"
for the email field to show the appropriate on-screen keyboard on mobile devices.
Use the required attribute for your email and password field so you can warn of invalid values when the user submits the form.
Consider using real-time validation
to help users correct invalid data as soon as they have entered it, rather than waiting for form
submission.
Ensure users can see the password they entered
The text you fill in for <input type="password"> is obscured by default,
to respect the privacy of users.
Help users to enter their password,
by showing a <button> to toggle the visibility of the entered text.
{% Codepen {
user: 'web-dot-dev',
id: 'bd8577c5380c436dba2788c7a2c8652a',
height: 300
} %}
Learn more about
implementing a password-reveal <button>.
Ensure your sign-in and sign-up forms are usable
Test your sign-in and sign-up forms regularly with real people to make sure that authentication works as expected. Use analytics and real user measurement (RUM) to collect field data, and tools such as Lighthouse and PageSpeed Insights to run tests yourself. Learn more about testing for usability and gathering analytics data.
Ensure your forms work in different browsers and on different platforms. Test your form on different screen sizes, using only your keyboard, or using a screen reader such as VoiceOver on Mac or NVDA on Windows.
Help users change their account settings
Make sure that users can change every account setting, including email addresses, passwords, and usernames.
Make it transparent what data you are storing, and help users download all their personal data at any time. Ensure users can delete their account if that's what they want. Account management features such as these may be a legal requirement in some regions.
Ensure users can update their passwords
Make it easy for users to update their password.
{% Codepen {
user: 'web-dot-dev',
id: 'dd3ac6198324f644774583c02bf0fbd8',
height: 450
} %}
Ask users for the current password before changing it, and send an email about a password change with the option to revert and lock the account.
Add the option to request a new password,
and consider providing a .well-known URL for requesting a new password.
Resources
- Password Storage Cheat Sheet
- Understanding the root cause of account takeover
- Password security: Complexity vs. length
- Sign-up form best practices
- Sign-in form best practices
- Effective password management | Session
Internationalization and localization
If you're reading this, you're using the World Wide Web. Your forms may be used by people speaking different languages, people from different countries, and people with different cultural backgrounds. Learn how to prepare your form for internationalization and localization.
Ensure your form works in different languages
Let's see how you can make sure your form works with different languages.
The first step to make your site localization-ready is to define the language attribute lang on the <html> element.
This attribute enables screen readers to invoke the correct pronunciation,
and helps browsers offer a translation of the page if the defined language is not the default browser language.
<html lang="en-us">
Learn more about the lang attribute.
Say you translated a form to German.
How can you make sure search engines and browsers know about the translated version?
You can add <link> elements in your site's <head> describing the alternate versions.
<link rel="alternate" title="The form element"
href="https://example.com/en/form" hreflang="en">
<link rel="alternate" title="Das Formularelement"
href="https://example.com/de/form" hreflang="de">
Help users who speak another language to use your form
You can't translate your form into every language, but you can ensure translation tools can translate it for you.
{% Aside %} Try it out! Enter the URL of your form in Google Translate, choose a language, and translate it. All visible content of your site will be translated. This is a good test to see if every text is translatable. {% endAside %}
To ensure translation tools translate all the text on your form, make sure all text is defined in HTML and is visible. Some tools also work with content defined in JavaScript, but to improve compatibility, try to include as much text as possible in HTML.
{% Aside %}
Avoid using the aria-label attribute to describe elements.
Many translation tools aren't able to translate the aria-label attribute.
Learn more why aria-label doesn't translate.
{% endAside %}
Ensure your form works with different writing systems
Different languages use different writing systems and character sets. Some scripts are written from left to right, and some from right to left.
Make spacing independent of writing systems
To ensure your form works for different writing systems, you can use CSS logical properties.
{% Codepen { user: 'web-dot-dev', id: '043b19ee429ea0834022967e2207d6b9', height: 300 } %}
The input has a border thickness of 1px on all sides,
except on the left side, where the border is 4px thick.
Now, edit the CodePen and change the writing system to right to left
by adding dir="rtl" to the <main> element
{% Aside %}
The dir attribute indicates the direction of an element's text.
If the complete site uses the same writing system,
define the dir attribute on the <html> element.
In CodePen you don't have access to the <html> element,
which is why you need to add it to <main>.
You can also use the CSS direction property to achieve the same result.
{% endAside %}
The thick border is now on the right side. That's because we defined the border using a logical property.
input {
border-inline-start-width: 4px;
}
Learn more about logical properties.
Ensure your form can handle different name formats
Say you have a form where the user should fill in their name. How would you add the field to your form?
You could add one field for the first name and one for the surname. However, names are different around the world: for example, some people don't have a surname—so how should they fill in the surname field?
To make it quick and easy to enter names—and to ensure everybody can enter their name, whatever the format—use a single form field for names wherever possible.
Learn more about personal names.
If you have a name with non-Latin characters,
you may have encountered the issue that your name is reported as invalid in some forms. When you
build forms, make sure to allow all possible characters—and do not assume that a name only consists
of Latin characters.
Allow a variety of address formats
The headquarters of Google is at 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States.
This address includes the street number, street, city, state, postal code, and country. In your country, the address format may be totally different. How can you ensure everybody can enter their address in your form?
One way is to use generic inputs.
{% Codepen { user: 'web-dot-dev', id: '4d4096f67fe84bc2a8c7640974a0fb21', height: 700 } %}
Learn more about other ways to work with international address fields.
{% Assessment 'internationalization' %}
Resources
- W3C Internationalization articles and tutorials
- Frank's Compulsive Guide to Postal Addresses provides useful links and extensive guidance for address formats in over 200 countries.
- DataHub.io is a tool for downloading country codes and names.
JavaScript
You can use JavaScript to respond to user interactions on your form, reveal additional form fields, submit a form, and much more.
Help users fill in additional form controls
Imagine that you built a survey form. After a user selects one option,
you want to show an additional <input> to ask a specific question related to the selection.
How can you only show the relevant <input> element?
{% Codepen { user: 'web-dot-dev', id: '8e1e7a38790c75c267a978efa1d8e937', height: 350 } %}
You can use JavaScript to reveal an <input> only when the associated <input type="radio"> is currently selected.
if (event.target.checked) {
// show additional field
} else {
// hide additional field
}
{% Aside 'caution' %} Make sure your form is still usable if JavaScript isn't available. The core experience should be the same for all users, with JavaScript used only as an enhancement. {% endAside %}
Let's look at the
JavaScript code for the demo.
Have you noticed the aria-controls, and aria-expanded attributes?
Use these
ARIA attributes
to help screen reader users understand when an additional form control is shown or hidden.
{% Aside 'caution' %}
The aria-expanded attribute for <input type="radio"> isn't
correctly announced by every screen reader.
{% endAside %}
Ensure users can submit a form without leaving a page
Imagine you have a comment form. When a reader adds a comment and submits the form, it would be ideal if they could immediately see the comment without a page refresh.
To achieve this, listen to the onsubmit event, then use event.preventDefault() to prevent the default behavior,
and send the FormData using the Fetch API.
{% BrowserCompat 'api.fetch' %}
{% Aside %}
FormData is a set of key/value pairs representing form fields and their values.
You can add all your form fields to the FormData object, or only send some form fields and their values.
{% endAside %}
{% Codepen { user: 'web-dot-dev', id: 'b499a861d7b1fee74464a690cf0ff97e', height: 400, tab: 'js,result' } %}
Your backend script can check if a POST request appears to be from the browser
(using the action attribute of a form element, where method="post") or from JavaScript,
such as a fetch() request.
if (req.xhr || req.headers.accept.indexOf('json') !== -1) {
// return JSON
} else {
// return HTML
}
Always notify screen reader users about dynamic content changes.
Add an element with the aria-live="polite" attribute to your HTML,
and update the content of the element after a change.
For example, update the text to 'Your comment was successfully posted', after a user submits a comment.
Learn more about ARIA live regions.
Validation with JavaScript
Ensure error messages align with your site style and tone
The wording of default error messages differs between browsers.
How can you make sure the same message is shown to all users,
and that the message aligns with your site's style and tone?
Use the setCustomValidity()
method of the Constraint Validation API
to define your own error messages.
{% Codepen { user: 'web-dot-dev', id: '7ea31257d7cd8fc28792c7f5cdaba97b', height: 300, tab: 'js,result' } %}
{% Aside %} Make sure to translate and localize your error messages if you have a multi-language website. {% endAside %}
Ensure users are notified about errors in real time
The built-in HTML features for form validation are great for notifying users about invalid form fields before the data is sent to your backend. Wouldn't it be great to notify users as soon as they leave a form field?
{% Codepen { user: 'web-dot-dev', id: 'b7ed22a0539f9beef4dc03380f51f224', height: 300, tab: 'js,result' } %}
Listen for the
blur
event which fires when an element loses focus, and use the
ValidityState interface to detect if a form control is invalid.
Ensure users can see the password they entered
The text entered for <input type="password"> is obscured by default,
to respect the privacy of users.
Help users to enter their password, by showing a <button> to toggle the visibility of the entered text.
{% Codepen { user: 'web-dot-dev', id: 'bd8577c5380c436dba2788c7a2c8652a', height: 300 } %}
Try out the demo. Toggle the
visibility of the entered text, by using the Show Password <button>.
How does this work? Clicking on Show Password,
changes the type attribute of the password field from type="password" to type="text",
and the <button> text changes to 'Hide Password'.
{% Aside 'codelab' %}
Try to improve the Show Password <button>.
Where would you position the <button>?
Could you use only an icon and still make sure the <button> is accessible?
{% endAside %}
It's important to make the Show Password button accessible.
Connect the <button> with the <input type="password"> using the aria-controls attribute.
{% Aside 'caution' %}
The aria-controls attribute is not supported by every screen reader,
but for those that do support it, it's a great enhancement.
{% endAside %}
To notify screen reader users if the password is currently shown or hidden,
use a hidden element with aria-live="polite", and change its text accordingly.
It's important to enable screen reader users to know when a password is displayed and visible to someone else looking at their screen.
<span class="visually-hidden" aria-live="polite">
<!-- Dynamically change this text with JavaScript -->
</span>
{% Aside %}
Microsoft Edge shows a built-in password reveal control for <input type="password">. To prevent having two reveal buttons, hide the built-in control with:
::-ms-reveal {
display: none
}
Learn more about customizing the password reveal button. {% endAside %}
Learn more about implementing a show password option.
Resources
Payment forms
{% Aside %}
This module is about payment forms and doesn't explain how to implement transactions on your site.
You can add payment functionality by implementing Web Payments,
or using a third-party payment platform.
{% endAside %}
The payment form is often the last step before completing a purchase. To maximize conversions, ensure your payment form is user-friendly and secure.
{% Codepen {
user: 'web-dot-dev',
id: '0f0da240784a36da7fa958018af29f3f',
height: 500
} %}
Ensure users know what to fill in
Keep your payment form as simple as possible, showing only required fields.
{% Aside %}
You don't need to add a selector for the card type—that's worked out automatically by the payment processor from the card number.
However, you may want to enhance your card number field by indicating the card type based on the entered number.
You can use a regex to test the card type,
and show the brand logo next to the card number <input>.
{% endAside %}
Indicate the payment amount. The Submit button is ideal for that.
<button>Pay $300.00</button>
Use self-explanatory wordings for your <label> elements.
For example, use 'Security code',
instead of an acronym like 'CVV' that's only used by some brands.
{% Aside %}
Use a single <input> for each of the name and card number fields. Keep the user in typing mode,
and don't waste their time by forcing them to jump between multiple name or card number fields.
{% endAside %}
Help users enter their payment details
To maximize conversions, ensure users can fill out your payment form as quickly as possible.
Use inputmode="numeric" for the card number and security code fields
to show an optimized on-screen keyboard for entering numbers.
{% Aside 'caution' %}
Use type="number" only for incremental fields,
for example, the quantity of a product.
Browsers show an up/down arrow for type="number"
which makes no sense for payment card numbers.
{% endAside %}
Add appropriate autocomplete values for your payment form controls to ensure browsers offer autofill.
Use autocomplete="cc-name" for the name,
autocomplete="cc-number" for the card number, and autocomplete="cc-exp" for the expiry date.
{% Aside %}
You can help users fill in the correct format for expiry date by using an
input mask.
Test with real users, using only your keyboard,
and a screen reader such as VoiceOver
on Mac or NVDA on Windows to ensure the input is still accessible.
{% endAside %}
Ensure users enter data in the correct format
Use the required attribute for every <input> to ensure users fill out the complete form.
Payment card security codes can be three or four digits.
Use minlength="3" and maxlength="4" to only allow three and four digits.
Ensure users only enter numbers for the card number and security code.
Use pattern="[0-9 ]+" to allow users to include spaces when entering a card number,
since this is how the numbers are displayed on the physical cards.
{% Aside %}
Payment card brands use different formats,
for example, for card numbers and security codes.
Always test your payment form with every card type you support.
Ensure your validation rules consider every possible format.
{% endAside %}
Resources
Security and privacy
When you create a form, you work with user data. Your number one concern should be to ensure that user data is kept private and transferred securely. Let's have a look at what can be done.
{% Aside %} A secure form means that all data is encrypted, kept secure, and no unauthorized access can happen.
To ensure privacy, only save the data you need, only save personal data after consent, ensure the user is in full control of their data, and never share user data with others without the user's consent. {% endAside %}
Ensure your form is secure
As a first step, make sure to request as little data as possible. Don't ask for data you don't need, and always question whether you need all of the requested data. Less data means less risk, less cost, and less liability. In addition, reducing the number of fields in a form makes it less complex, easier to fill out, and can reduce abandonment rates.
Always use HTTPS, especially for pages that include a form. With HTTPS, data is encrypted when coming from the server and when going back to the server.
Say you're sitting in a café using public Wi-Fi. You open an e-commerce site and fill in your credit card information to purchase something. If the website uses HTTP, anyone (with the skills to do so) on the same public Wi-Fi could see your credit card information. If the website uses HTTPS, the data is encrypted and therefore protected from anyone trying to access it.
On your site, you should also make sure to redirect any HTTP requests to HTTPS. Learn more about how to redirect all traffic to HTTPS.
Help users to keep their data private
In the first module, you learned about two possible ways to transfer data:
using a GET request and using a POST request.
With a GET request, form data is included as a query string
in the request URL. If you submit a form that uses a GET request,
the browser adds the request URL including form data to your browsing history.
Convenient if you want to look up past form submissions,
for example for a search form. Not great at all,
if sensitive data is submitted,
and everybody with access to your browser history or local network can see this information.
Use POST requests for every form where data that's personal or otherwise sensitive may be submitted.
This way, the data is only visible to the backend script processing it.
What about saving and processing personal data directly in the browser?
You could use client storage, for example, localStorage to store personal data in the browser.
With regard to privacy, this is less than ideal.
Again, everyone with access to your browser is able to read this information.
You should only store encrypted values for personal data.
Ensure users can safely sign up and sign in
User account authentication is a complex issue in terms of privacy and security. It may be better to use a third-party identity provider, instead of building your own secure authentication system.
Learn more about best practices for account authentication, and password management.
{% Aside %} If you do choose to build your own authentication system, make sure to follow best practice for password management:
- Never store or transmit passwords in plain text. Make sure to salt and hash passwords—and don't try to invent your own hashing algorithm.
- Ensure users enter secure passwords.
- Don't allow compromised passwords.
- Don't force password updates.
- Make it simple for users to change or reset passwords. {% endAside %}
Help users access their personal data
Many regions have laws and regulations regarding data protection and privacy, including the CCPA in California and the PDPA in India. Every website available in the European Union (EU) has to follow General Data Protection Regulation (GDPR), even if the site is not based in the EU.
GDPR sets guidelines for the collection and processing of personal information from people living in the EU. Consent is required to process personal data, users can request personal information that you store at any time, and you have to officially announce data leaks. A good thing for the user, as this helps to ensure that their privacy is respected. Learn more about GDPR.
Make sure your users know how you plan to process personal data. Transparency is the key to trust. Users should always be able to access, modify, and delete all data you saved for them.
Ensure users can update their personal data
Make it easy for users to update their personal data, including passwords, email addresses, and usernames. Notify users about changes to their stored personal data, and ensure users can revoke changes. For example, send an email to the previous and new email address after users change their email address.
{% Aside %} Separate the concept of user account and user identity to simplify implementing third-party identity provision. Allow users to change their username and link multiple identities to a single user account, for example if they sign up with an email and password, then sign up with a third-party identity provider. {% endAside %}
Make it easy for users to delete their account, including all associated data, and where relevant, make it possible to download data. Account deletion is a legal requirement in some regions.
{% Aside %} Link to your privacy policy on each page with a form, especially if personal data is processed. {% endAside %}
Require an additional authentication step, for example, re-entering the current password, to view or change personal information on your site.
Find out more: Web Application Privacy Best Practices.
Ensure all data is in good shape
In a previous module, you learned about validation on the frontend. Frontend validation is important, but users might still be able to submit invalid data. As a next step, you must also validate the data on the backend before saving the data in your database. This ensures that no invalid data is saved in your database.
Validation helps to ensure that the data format is valid, but you should still not trust data entered by users. How can you safely output the data? To prevent Cross Site Scripting (XSS), and ensure all data is safe to include in HTML, you must sanitize data before output.
Learn more about sanitizing data before output and, where possible, use the Sanitizer API.
Ensure all submissions come from real people
To help protect your data, you have various options to prevent spam submissions from bots.
The first option is to use a service such as reCAPTCHA, to distinguish between real people and bots. This requires you to include a JavaScript snippet on your page, and add extra attributes to your Submit button.
reCAPTCHA performs various checks to find out if you are a human. For example, it may ask you to identify images. Automated software, such as a bot, cannot accomplish these challenges and can't submit the form.
{% Aside %} Always make sure your spam protections are accessible. Try them out by using a screen reader, and by only using your keyboard. The best spam protection is useless if it makes the form unusable for real people. {% endAside %}
A honeypot
{% Codepen { user: 'web-dot-dev', id: 'c8e82763c4ce344eb23dfd3ef621b662', height: 400, tab: 'html,result' } %}
Another option is to use a so-called 'honeypot': a visually hidden form field. Humans won't see a honeypot field, but bots will fill it in. On the backend, your processing script can check if the field was completed. If it was, the submission was probably from a bot, and you can ignore it.
There are also services like Akismet, which can help you with spam protection. The Akismet filter works by combining information about spam captured on all participating sites, and then using those spam rules to block future spam. Akismet is transparent to the user, and catches most spam.
{% Assessment 'security-privacy' %}
Resources
Styling forms
To ensure that your form is accessible to as many people as possible, use the elements built for the
job: <input>, <textarea>, <select>, and <button>. This is the baseline for a usable form.
{% Codepen { user: 'web-dot-dev', id: '9d0576454b3b2d0fc001addab70d25bc', height: 250 } %}
The default browser styles don't look great! Let's change that.
{% Aside %} Progressive enhancement is a strategy that provides a baseline of essential content and features for as many users as possible. It ensures the best possible experience for users on modern browsers.
You start with content, use semantic HTML, add future-proof CSS, and add robust JavaScript as a last step. {% endAside %}
Ensure form controls are readable for everyone
The default font size for form controls in most browsers is too small. To ensure your form controls are readable, change the font size with CSS:
{% Codepen { user: 'web-dot-dev', id: '477f5e58a406c6f86bfbfc9da1f18a69', height: 300 } %}
Increase the font-size and line-height to improve readability.
.form-element {
font-size: 1.3rem;
line-height: 1.2;
}
{% Aside %}
For font-size use relative units such as em (relative to the base size of the element's parent)
or rem (relative to the base size of the document) to ensure that size responds to user preference.
Users can change the base font-size and all elements with a relative font-size will adjust
automatically. For line-height use a unitless value
such as 1.5, to keep the line height relative to the font size.
Learn more about pixels vs. relative units in CSS. {% endAside %}
Help users navigate through your form
As a next step, change the layout of your form, and increase the spacing of form elements, to help users understand which elements belong together.
{% Codepen { user: 'web-dot-dev', id: 'f2aca03916816074010e896f064f456a', height: 450 } %}
The margin CSS property increases space between elements,
and the padding property increases space around the element's content.
For the general layout, use Flexbox or Grid. Learn more about CSS layout methods.
Ensure form controls look like form controls
Make it easy for people to fill out your form by using well-understood styles for your form controls.
For example, style <input> elements with a solid border.
{% Aside %}
The default <input> border color is too light in many browsers.
The lack of contrast can make the element hard to see, especially on mobile.
Open this demo in Chrome on Android to see the default styles.
{% endAside %}
{% Codepen { user: 'web-dot-dev', id: 'd0740e968b5c72a0cf180359e80f5efc', height: 500 } %}
input,
textarea {
border: 1px solid;
}
Help users submit your form
Consider using a background for your <button> to match your site style,
and override or remove the default border styles.
{% Codepen { user: 'web-dot-dev', id: 'f0052d40806fd750362fa0abaab01dcf', height: 500 } %}
{% Aside %}
In modern browsers, you can style a <button> like any other element,
so you should always use a semantic <button>, or <input type="submit">.
Using the element built for the job provides many built-in usability and accessibility
benefits that you won't get when using a generic element such as a <div>.
You'll learn about the built-in features in other modules.
Learn more about
reverting the default style of a <button>.
{% endAside %}
Help users understand the current state
Browsers apply a default style for :focus.
You can override the styles for :focus to match the color to your brand.
button:focus {
outline: 4px solid green;
}
{% Aside %}
Only remove the outline on :focus if you also add other appropriate focus styles,
to ensure that default and focus styles are distinguishable.
Learn more about designing focus indicators. {% endAside %}
{% Assessment 'styling' %}
Resources
Styling form controls
In this module you learn how to style form controls, and how to match your other site styles.
{% Aside 'caution' %}
Styling HTML form controls can be a challenge,
but you should still use built-in elements wherever possible.
Elements such as <input> and <button> are widely supported across browsers and platforms,
and have built-in features that enhance usability and accessibility,
which you don't need to implement yourself. Using a <div> instead doesn't provide these benefits.
{% endAside %}
Help users select an option
The <select> element
The default styles of a <select> element don't look great, and the appearance is inconsistent between browsers.
{% Codepen { user: 'web-dot-dev', id: 'eea33ada7ec976aa5791a5cd14756c34', height: 300 } %}
{% Aside %}
You can also use an <input> in combination with the
<datalist> element.
This gives you a combination of a text field and a list of <option> elements.
You can see examples of <datalist> here.
{% endAside %}
First, let's change the arrows.
select {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background-color: #fff;
background-image: url(arrow.png);
background-repeat: no-repeat;
background-position: right .7em top 50%, 0 0;
background-size: .65em auto;
}
To remove the default arrows of a <select> element,
use the CSS appearance property.
To show the arrow of your choice, define the arrow as a background.
{% Aside %}
To ensure the best cross-browser compatibility,
you must also include the prefixed versions of appearance: -moz-appearance and -webkit-appearance.
Learn more about vendor prefixes. {% endAside %}
You should also change the font-size to at least 1rem
(which for most browsers has a default value of 16px) for your <select> element.
Doing so will prevent a page zoom on iOS Safari when the form control is focused.
You can, of course, also change the element colors to match your brand colors.
After adding some more styles for spacing, :hover, and :focus,
the appearance of the <select> element is now consistent between browsers.
See this in the following demo from Styling a Select Like It’s 2019
{% Codepen { user: 'web-dot-dev', id: '2f6101907a292463bccd8d7a72d09554', height: 300 } %}
What about the <option> element? The <option> element is a so-called
replaced element whose representation is outside the scope of CSS.
As of this writing, you can't style the <option> element.
{% Aside %}
There is an ongoing proposal to allow web developers to style and extend built-in web UI controls,
including <select> and <option> elements. This would make it easier to style form controls in the future.
Learn more about Open UI. {% endAside %}
Checkboxes and Radio buttons
The appearance of <input type="checkbox"> and <input type="radio"> varies across browsers.
{% Codepen { user: 'web-dot-dev', id: '74d28931d0c0e9aacc89f62380f365e4', height: 400 } %}
Open the demo on various browsers to see the difference. Let's see how to make sure that checkboxes and radio buttons match your brand and look the same cross-browser.
In the past, developers could not style <input type="checkbox"> and <input type="radio"> controls directly.
Checkboxes and radio buttons can be styled via their pseudo elements, now.
Or the following replacement technique can be used to create custom styles for these elements.
First, hide the default checkbox and radio button visually.
input[type="radio"],
input[type="checkbox"] {
position: absolute;
opacity: 0;
}
It's important to use position: absolute in combination with opacity: 0 instead of display: none or visibility: hidden
so that the controls are only visually hidden. This will ensure they are still exposed by the browser's
accessibility tree. Note that further styling may be needed to ensure that the visually hidden
form controls remain positioned "on top" of their replacement elements. Doing so will help ensure that hovering over one of these
elements, when a screen reader is on, or when using touch devices with screen readers enabled, the visually hidden controls will
be discoverable if exploring by touch, and the screen reader's visible focus indicator will generally appear in the location the controls
are rendered on screen.
To show your custom checkboxes and radio buttons, you have different options.
You use the ::before CSS pseudo-element and the CSS background property, or use inline SVG images.
input[type="radio"] + label::before {
content: "";
width: 1em;
height: 1em;
border: 1px solid black;
display: inline-block;
border-radius: 50%;
margin-inline-end: 0.5em;
}
input[type="radio"]:checked + label::before {
background: black;
}
{% Aside %} Styled form controls must be easy to understand and use. People are used to a checkbox that looks like a checkbox, so make sure when you style a form control that users still understand how to use it. {% endAside %}
There are a lot of possibilities with CSS to ensure checkboxes and radio buttons match your brand styles.
{% Codepen { user: 'web-dot-dev', id: '433b6d50b9d7d706e31ee5a638790c3d', height: 400 } %}
Learn more about
styling <input type="checkbox">, and <input type="radio">
and custom checkbox styles.
How to use your site's colors for form controls
Do you want to bring your site styles to form controls with one line of code?
You can use the accent-color CSS property to achieve this.
checkbox {
accent-color: orange;
}
{% Aside %}
As of this writing, only Chrome, Firefox and Edge support accent-color.
To ensure cross-browser compatibility,
you might want to use workarounds until accent-color is supported in all platforms.
{% endAside %}
{% Assessment 'styling-form-controls' %}
Resources
Test your forms
In previous modules, you learned how to build a form, help users avoid re-entering data, and how to validate form data. How can you now make sure the form is usable? You can test and analyze your form to answer this question.
Does your form work on other devices?
You begin by confirming that your form is working on your own device. However, there are many types of devices your users may use. How can you test if your form works with other devices?
First, test your form on a desktop device. Then try only using your keyboard. Next, test it on a phone. You have now tested your form with different input methods (keyboard, touch, and mouse), different screen sizes, different browsers and different operating systems.
Do you have more devices you can test on? Test your form on all of them. The more different devices, browsers, browser versions, and operating systems you can test on, the better!
You can also use a test service to test your form on lots of different browsers, different browser versions, different devices, and different operating systems. BrowserStack offers free test accounts for open source projects, to enable testing on different browsers, devices and operating systems.
How can you test if your form works for others?
Your first form is ready. You spend a lot of time making sure it works well. How can you confirm that your form is usable for everybody else? As a first step, you can ask your friends and colleagues to fill out your form.
Sit next to each other or share a screen. This way, you can see how they interact with your form. Watch them fill out the form. Ask them to say out loud what they're doing and if they're experiencing any problems. After they complete the task, ask them questions. Was it clear what they should fill out? Did they have any issues filling out the form? Was anything unclear? These questions help you build even better forms.
{% Aside %} People from all over the world will use your form. Do you have an address field in your form? Google a city you always wanted to visit, and search for a restaurant there. Now enter the address of the restaurant in your address field. The format of the address may be totally different from the address format where you live. Test with different data to ensure your form can handle diverse formats. {% endAside %}
How can you measure how your form performs?
You confirmed that your form is usable for other people. As a next step, you should measure how your form performs. There are free tools available for this. Let's have a look at some of them.
{% Aside %} You should always measure before and after you change your form. This way, you can compare the results, measure the impact of your change, and demonstrate improvements. {% endAside %}
PageSpeed Insights (PSI)
PSI measures the performance of your site and gives you hints on how to improve it.
Try it out: PageSpeed provides a performance report using lab and field data. A fast site is the first sign that your form is usable. Your site isn't fast yet? PSI shows you recommendations on how to improve performance.
Look again at the report of your site you tested before with PSI. See the information about Core Web Vitals (CWV)? This is a summary if your site fulfills the CWV criteria. CWV helps you understand how users experience a web page.
Lighthouse
Lighthouse helps you identify performance, search engine optimization (SEO), best practice, and accessibility issues.
There are different ways to use Lighthouse. One option is to run it directly in DevTools. Open the URL with your form in Chrome, open DevTools, switch to the Lighthouse tab, and run the audit.
The performance metrics from PSI are displayed. In addition, Lighthouse checks your site against SEO, best practice, and accessibility issues. Forgot to connect a label to a form control? Lighthouse warns you and provides you with a guide to correct the issue.
Tools to identify common issues
There are many tools to identify common issues.
One way is to use the
Form troubleshooter extension.
It warns you about missing autocomplete attributes,
non-standard attributes, missing or empty labels, and more.
You can also use an accessibility evaluation tool like WAVE or axe. These tools notify you about missing labels, missing connections between labels and form controls, insufficient color contrasts, and many more accessibility issues.
{% Aside %} All these tools help you find common issues, but they aren't able to detect all issues. Always test your forms manually and with real people. {% endAside %}
How can you monitor form experience?
Monitoring real user experience of your forms helps you identify new issues quickly. Let's see how you can monitor your form.
{% Aside %} Lab data is measured with a controlled test under ideal conditions. Field data comes from real people. With field data, you get metrics for real usage in real-world conditions.
People use different browsers, operating systems, screen resolutions, and connection speeds. They use different devices. With field data, you collect metrics from a variety of different conditions.
Learn more about why lab and field data can be different. {% endAside %}
PSI
One way to monitor experience is to use PSI again. You can use the PSI API to build your own monitoring tool: The PageSpeed Insights API explains how to do this.
PSI includes data from the Chrome User Experience Report (CrUX) dataset, if available for the given URL. This way, you can also see field data directly in the report.
Lighthouse
Lighthouse can be used as a command line tool, as a Node module, or with the Lighthouse CI tools. Performance monitoring with the Lighthouse CI explains adding Lighthouse to a continuous integration system, such as GitHub Actions.
There are many more tools available to measure and monitor your site. Some are available as web tools, some let you run the audit on your command line, and some offer an API to integrate them into your tools.
How to analyze your form
You tested your form with real users, and learned how to measure and monitor it. How can you collect statistics about your users and how they interact with your form? You can use an analytics tool. Let's have a look at one and how this works.
Analytics
One tool you can use is Google Analytics (GA). After setting it up, you get a JavaScript snippet you include in each page on your site. From this point on, you can find out how people use your site.
How many people visit the page with your form on it? How many fill out the form and move to the next page? You get answers to these questions by using analytics tools.
{% Aside %} Monitoring the real-world experience of your users is sometimes called Real User Monitoring (RUM).
RUM collects performance metrics directly from the end user's browser. RUM data shows you how your site performs, and how real users interact with it. {% endAside %}
You can also set up more advanced analytics reports. Want to track how many users click the Submit button? You can define and integrate events to analyze this.
A wide range of analytics tools is available. Some are minimalistic, some offer a lot of options for individualization. Try out a range of tools to find the best for your needs.
{% Assessment 'testing' %}
Resources
- Getting started with measuring Web Vitals
- Core Web Vitals
- Why lab and field data can be different
- Google Analytics: About Events
How to test forms for usability
Usability testing is a technique to evaluate a device or product, such as a web form. By testing your form on real users, you can identify issues that automated tools may not find. You learn first-hand where you need to improve your form.
To conduct a usability test, you give a person a task such as asking them to complete a form, watch them doing the task, then ask them to 'think aloud'. During the test, you take notes about their interactions. After the test, you ask questions. This way, you find out where your form needs improvements.
Test your forms with real people
First, you need to find people who can test your form. Ask family members, friends, and colleagues to test your form.
{% Aside %} Testing doesn't have to be difficult. To improve your forms you don't need a usability lab and complex reporting methodologies!
Testing with one single other person is likely to help you improve your forms. Testing with two or more is even better. Take simple notes and make changes accordingly. {% endAside %}
Try to get as diverse a range of testers as possible: in particular, think about age, and technical experience.
Users may be situationally, temporarily, or permanently impaired. For example, a user could have a vision impairment that makes it difficult for them to read text without sufficient contrast, or a user may have a temporary arm injury that requires them to fill out the form with one hand.
Testing with a diverse range of people helps you find usability issues you may not be aware of. Learn more about inclusive design, the linked site provides personas and sample activities to help you with inclusive UX testing.
For a high-traffic site, you should work with an accessibility consulting company that can provide comprehensive testing with assistive technologies and expert advice on fixing any issues found.
Identify issues
You found people to test your form. How does a usability test work in practice?
Ask people to complete specific tasks. Say you want to test your form. Ask them to fill out your form. Are they able to locate the form from the homepage? Are they able to successfully submit the form? Ask them about their experience. As they fill out the form, ask them to explain what they're doing and any issues they encounter.
Ensure your form is accessible
How can you make sure your form is accessible to all your users?
The best way to ensure your form works as expected is by testing it with real people. Ask a friend or colleague to complete your form. You may be surprised how differently other people use your form, and what is important for them.
Finding people with different needs, who can test your form, isn't always possible. You should, however, always do an accessibility review of your own. For example, you can emulate vision deficiencies, use a screen reader on your phone or computer, or use only your keyboard to fill out your form. Try navigating your form via a screen reader such as VoiceOver on Mac or NVDA on Windows.
Learn more about doing an accessibility review.
Measure and monitor impact of changes
After testing with real people and gathering feedback, it's time to adapt your form to the needs of your users. How can you be sure the new version is better than the previous one?
You can ask the same people who tested your form before, to test your form again. Give them the same tasks, and ask the same questions afterwards. Was it easier to use this time? Did they encounter any new issues?
You tested your forms at home or in the office. How can you test and monitor the experience of your end users? Use analytics, and real user measurement (RUM). Implement goal funnels in analytics to check if changes reduce form abandonment and the time it takes for users to complete your form.
Test different form designs with end users
One way to get results quicker is by doing A/B testing. You create two versions of your form, and randomly provide one version or the other to your users: some people get version A, and some get version B. Comparing results for the two versions helps you decide which version to use for later iterations.
Learn more about A/B testing.
Resources
Help users enter the right data in forms
Browsers have built-in features for validation to check that users have entered data in the correct format. You can activate these features by using the correct elements and attributes. On top of that, you can enhance form validation with CSS and JavaScript.
{% Aside %} This module is about form validation on the frontend. You must also make sure to validate data before storing or sharing it on your backend server: find out more. {% endAside %}
Why should you validate your forms?
You learned in the previous module how to help users avoid having to repeatedly re-enter information in forms. How can you help users enter data that's valid?
It's frustrating to fill out a form without knowing which fields are required, or the constraints of those fields. For example, you enter a username and submit a form—only to find out that usernames must have at least eight characters.
You can help users with that by defining validation rules and communicating them.
Help users from accidentally missing required fields
You can use HTML to specify the correct format and constraints for data entered in your forms. You also need to specify which fields are mandatory.
{% Codepen { user: 'web-dot-dev', id: '1e3e72e4376292ab80ecd8955529806b', height: 300 } %}
Try to submit this form without entering any data.
Do you see an error message attached to the <input> telling you that the field is required?
This happens because of the required attribute.
<label for="name">Name (required)</label>
<input required type="text" id="name" name="name">
{% Aside %} Different browsers use different text for error messages. The wording is also dependent on the browser language of the user. If you use Safari and your preferred browser language is German, you get a different error message than a user in Chrome with English as their chosen browser language.
Find out how to change the default error messages with JavaScript later in this module. {% endAside %}
You already learned that you can use many more types, for example, type="email".
Let's have a look at a required email <input>.
{% Codepen { user: 'web-dot-dev', id: '061add379d5159560d2bc6bc037d82da', height: 300 } %}
Try to submit this form without entering any data. Is there any difference from the demo before? Now insert your name in the email field and try to submit. You see a different error message. How is that possible? You get a different message because the value you entered isn't a valid email address.
The required attribute tells the browser that the field is mandatory.
The browser also tests if the entered data matches the format of the type.
The email field shown in the example is only valid if it's not empty and if the entered data is a valid email address.
Help the user enter the correct format
You learned how to make a field mandatory. How would you instruct the browser that a user must enter at least eight characters for a form field?
{% Codepen { user: 'web-dot-dev', id: '0901d502bbe2cc7c4aa0bd73068ddd96', height: 300 } %}
Give the demo a try. After your change, you should not be able to submit the form if you enter less than eight characters.
{% Details %}
{% DetailsSummary 'h3' %} Toggle answer {% endDetailsSummary %}
<label for="password">Password (required)</label>
<input required minlength="8" type="password" id="password" name="password">
The name of the attribute is minlength.
Set the value to 8 and you have the desired validation rule.
If you want the opposite, use maxlength.
{% Aside %}
For numerical input types use min and max to achieve the same result.
{% endAside %}
{% endDetails %}
Communicate your validation rules
<label for="password">Password (required)</label>
<input required minlength="8" type="password" id="password"
name="password" aria-describedby="password-minlength">
<div id="password-minlength">Enter at least eight characters</div>
Make sure all users understand your validation rules.
For this, connect the form control with an element that explains the rules.
To do so, add an aria-describedby attribute to the element with the id of the form.
Pattern attribute
Sometimes you want to define more advanced validation rules.
Again, you can use an HTML attribute.
It's called pattern, and you can define a
regular expression as the value.
<label for="animal">What is your favorite animal? (required)</label>
<input required pattern="[a-z]{2,20}" type="text" id="animal" name="animal">
Here, only lowercase letters are allowed; the user has to enter at least two characters, and not more than twenty.
How would you change the pattern to also allow uppercase letters?
Try it out.
{% Details %}
{% DetailsSummary 'h3' %} Toggle answer {% endDetailsSummary %}
The correct answer is pattern="[a-zA-Z]{2,20}".
{% endDetails %}
Add styles
You have now learned how to add validation in HTML. Wouldn't it be great if you could also style form controls based on the validation status? This is possible with CSS.
How to style a required form field
Show the user that a field is mandatory before they interact with your form.
{% Codepen { user: 'web-dot-dev', id: '5a5de1e7dbdcdba86d19e30d42291a27', height: 300, tab: 'css,result' } %}
You can style required fields with the :required CSS pseudo class.
input:required {
border: 2px solid;
}
Style invalid form controls
Do you remember what happens if data entered by the user is invalid? The error message attached to the form control appears. Wouldn't it be great to adapt the appearance of the element when this happens?
{% Codepen { user: 'web-dot-dev', id: 'd6bb0f27d1faac3ffd3b594a81fad396', height: 300, tab: 'css,result' } %}
You can use the :invalid pseudo-class
to add styles to invalid form controls.
In addition, there is also the :valid pseudo-class for styling valid form elements.
{% Aside %}
Sometimes designers color the invalid state red,
using the :invalid pseudo-class.
However, to communicate error or success you should never rely only on color.
For people with red-green color blindness a green and a red border look almost the same.
It's impossible for them to see if there was an error or success. Add text or an icon to make it obvious.
{% endAside %}
There are more ways to adapt your styles based on validation. In the module about CSS you will learn more about styling forms.
{% Aside %}
In practice :invalid is tricky to work with.
Invalid form fields are already marked as :invalid before user interaction,
which may confuse users. The :user-invalid pseudo-class solves this issue,
as the styles are only applied after user interaction.
Learn more about :user-invalid.
{% endAside %}
Validation with JavaScript {: #javascript}
To further enhance validation of your forms you can use the JavaScript Constraint Validation API.
Provide meaningful error messages
You learned before that error messages are not identical in every browser. How can you show the same message to everyone?
To achieve this, use the
setCustomValidity()
method of the Constraint Validation API.
Let's see how this works.
const nameInput = document.querySelector('[name="name"]');
nameInput.addEventListener('invalid', () => {
nameInput.setCustomValidity('Please enter your name.');
});
Query the element where you want to set the custom error message.
Listen to the invalid event of your defined element.
There you set the message with setCustomValidity().
This example shows the message Please enter your name. if the input is invalid.
{% Codepen { user: 'web-dot-dev', id: '7ea31257d7cd8fc28792c7f5cdaba97b', height: 300 } %}
Open the demo in different browsers, you should see the same message everywhere. Now, try to remove the JavaScript and try again. You see the default error messages again.
There is much more you can do with the Constraint Validation API. You’ll find a detailed look at using validation with JavaScript in a later module.
How to validate in real-time
You can add real-time validation in JavaScript by listening to the onblur event of a form control,
and validate the input immediately when a user leaves a form field.
{% Codepen { user: 'web-dot-dev', id: 'b7ed22a0539f9beef4dc03380f51f224', height: 300, tab: 'js,result' } %}
Click the form field in the demo,
enter "web" and click somewhere else on the page.
You see the native error message for minlength below the form field.
Learn more about implementing real-time validation with JavaScript in an upcoming module.
{% Assessment 'validation' %}
Resources
Learn HTML
Welcome to HTML! HyperText Markup Language, or HTML, is the backbone of the web, providing the content, as well as the structure of that content, that you see displayed in your web browser.
Unless you're reading a PDF or a printed version of this page, this content is made up of various HTML elements and text. HTML is the content layer of the web. HTML elements are the nodes that make up the Document Object Model.
Cascading Style Sheets provide the look and feel, or presentation layer of the page. JavaScript is the behavior layer, often used to manipulate the objects within a document. Sites that are built with JavaScript frameworks are really just manipulating HTML. In turn, it's important to mark up your HTML in a way that scripts can easily parse and that assistive technologies can easily understand. This means writing HTML code with modern standards.
Overview
This HTML course for web developers provides a solid overview for developers, from novice to expert level HTML. If you're completely new to HTML, you will learn how to build structurally sound content. If you've been building websites for years, this course may fill in gaps in knowledge that you didn't even know you had.
Along this journey, we will be building the structure for MachineLearningWorkshop.com. No machines were harmed in the creation of this series.
This is not a complete reference. Each section introduces the section topic with brief explanations and examples, providing you an opportunity to explore further. There will be links to topic references, such as MDN and WHATWG specifications, and other web.dev articles. While this is not an accessibility course, each section will include accessibility best practices and specific issues, with links to deeper dives on the topic. Each section will have a short assessment to help people confirm their understanding.
Here's what you'll learn:
{% include 'partials/course-index.njk' %}
So, are you ready to learn HTML? Let's get started.
{% Aside %} This course was written by Estelle Weyl, with input and review from Rachel Andrew, and Jhey Tompkins. {% endAside %}
HTML APIs
In the introduction to this series it says "HTML elements are the nodes that make up the Document Object Model." We've discussed the type of element nodes. In this section we discuss element APIs that enable querying those nodes.
The DOM and AOM
The DOM is an API for accessing and manipulating documents. The DOM is the tree of all the nodes in the document. Some nodes can have children, others can't. The tree includes elements, along with their attributes, and text nodes.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/xsvSa8YkwHyPWNnQ8Tpi.png", alt="MLW Node tree showing elements and text nodes.", width="800", height="431" %}
Browser tools don't provide tree visualizations like the one above, but you can see the nodes in the element inspector.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/rX9myUwejCW2Ab0xUu0l.png", alt="The DOM/ARIA.", width="800", height="836" %}
The tree representation that can be inspected in browser developer tools is the accessibility tree. The AOM is based off the DOM; similarly, the accessibility tree contains objects representing all the markup elements, attributes, and text nodes:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/KMEuk7ub4ejE7zdr5Vyk.png", alt="An example of AOM.", width="800", height="527" %}
HTML Element APIs
The middle letter of DOM is "object." Just like the person or car object example from most intro to object-oriented programming
classes, every node in the document tree is an object that can be manipulated with JavaScript.
The browser provides numerous
APIs providing natively supported methods, events, and property querying and updating.
Element nodes contain information about all the attributes set on the element. You can use HTML interfaces to access
information about an element's attributes. For example, we can use HTMLImageElement.alt
get the alt attributes of all the images:
let allImages = document.querySelectorAll('img');
allImages.forEach((imageInstance) => {
console.log(imageInstance.alt);
});
HTML interfaces provide more than just access to an element's attributes; you can access a lot more information. We can
find the read-only HTMLElement.offsetHeight to get the height of each section in our page, relative to the layout.
let allSections = document.querySelectorAll('section');
allSections.forEach((sectionInstance) => {
console.log(sectionInstance.offsetHeight);
});
If the user changes their device orientation or otherwise changes the width of the viewport, the height of each <section>
will change and the DOM properties will automatically update with it.
The HTML interface APIs is not limited to accessing attribute values. The DOM provides insight into the current state of the UI.
HTML APIs can access all of that information. You can access the length of a video, where a view is in the current playback,
and if the video (or audio) has finished playing with HTMLMediaElement.duration,
HTMLMediaElement.currentTime, and
HTMLMediaElement.ended respectively.
Available element interfaces
Most HTML element we've covered thus far in this series and have yet to cover, other than some sectioning elements, have
an associated DOM interface. The base interface for all elements is aptly named Element.
The HTMLElement inherits from Element and all HTML element-specific
interfaces inherit from it. Some element-specific interfaces are implemented by multiple, similar elements.
The interfaces include:
HTMLAnchorElement-<a>HTMLAreaElement-<area>HTMLAudioElement-<audio>HTMLBaseElement-<base>HTMLButtonElement-<button>HTMLCanvasElement-<canvas>HTMLDataElement-<data>HTMLDataListElement-<datalist>HTMLDetailsElement-<details>HTMLDialogElement-<dialog>HTMLEmbedElement-<embed>HTMLFieldSetElement-<fieldset>HTMLFormElement-<form>HTMLHtmlElement-<html>HTMLIFrameElement-<iframe>HTMLImageElement-<img>HTMLInputElement-<input>HTMLLabelElement-<label>HTMLLegendElement-<legend>HTMLLIElement-<li>HTMLLinkElement-<link>HTMLMapElement-<map>HTMLMediaElement-<audio>,<video>HTMLMenuElement-<menu>HTMLMetaElement-<meta>HTMLMeterElement-<meter>HTMLModElement-<ins>,<del>HTMLObjectElement-<object>HTMLOListElement-<ol>HTMLOptGroupElement-<optgroup>HTMLOptionElement-<option>HTMLOutputElement-<output>HTMLPictureElement-<picture>HTMLProgressElement-<progress>HTMLQuoteElement-<q>,<blockquote>,<cite>HTMLScriptElement-<script>HTMLSelectElement-<select>HTMLSlotElement-<slot>HTMLSourceElement-<source>HTMLStyleElement-<style>HTMLTableCellElement-<td>,<th>HTMLTableColElement-<col>,<colgroup>HTMLTableElement-<table>HTMLTableRowElement-<tr>HTMLTableSectionElement-<thead>,<tbody>,<tfoot>HTMLTemplateElement-<template>HTMLTextAreaElement-<textarea>HTMLTimeElement-<time>HTMLTitleElement-<title>HTMLTrackElement-<track>HTMLVideoElement-<video>
The naming convention is "HTML" followed by an element or grouping of elements in upper camel case, followed by "Element", but the element or grouping of elements part follows no exact pattern. Don't worry. There is no need to memorize these. It's just important to know that they exist so you can look them up when you need to.
If you have a collection of elements, there are also come collection interfaces. For example, the
HTMLCollection.namedItem() method returns the first
element in the collection whose id or name attribute matches the parameter, or null if no element matches.
Over 30 elements don't have an associated DOM interface other than HTMLElement including <address>, <article>,
<section>, <nav>, <header>, <footer>, <aside>, and <hgroup>. Many elements that don't support any non-deprecated,
non-global attributes have element specific interfaces, like HTMLPElement (the <p> element) and HTMLUnknownElement
( <😃> or any other elements that are not defined), but those interfaces don't implement any additional properties or methods
on top of the properties and methods inherited from HTMLElement, and are not listed above.
Redundant API methods and properties
If an interface has the same method or property name as and interface it inherits, the inheriting method or property overwrites
the inherited one. When we accessed the alt and offsetHeight properties above with imageInstance.alt and sectionInstance.offsetHeight
respectively, the code didn't identify which API was being accessed.
Generally, as with these two examples, this isn't an issue. But, it can be. For example, the HTMLCollection.length
is read only, while the inheriting HTMLOptionsCollection interface's
length property (returned only by the options property of <select>) can also be used to set collection size.
Other interfaces
There are additional interfaces that enable manipulating the branch locations of DOM nodes. The EventTarget interface, which provides
us with addEventListener() and removeEventListener(), is inherited by the Node and Window interfaces. In turn, the Element, Document, and DocumentFragment (which we saw in custom elements) interfaces inherit from Node, and the HTMLElement interface inherits from Element.
The node interface
Every type of DOM node is represented by an interface based on Node,
which provides information and methods as elements relate to the DOM tree. The Node interface enables querying and adding nodes to the node tree.
Douglas Crockford's famous "walk the DOM" function, makes use of Node's firstChild
and the nextSibling properties.
const walk_the_DOM = function walk(node, callback) {
callback(node);
node = node.firstChild;
while (node) {
walk(node, callback);
node = node.nextSibling;
}
};
We used Node's appendChild() and
cloneNode() methods in defining custom elements.
The Node interface provides many useful properties and methods for querying and manipulating the DOM.
customElements.define('star-rating',
class extends HTMLElement {
constructor() {
super(); // Always call super first in constructor
const starRating = document.getElementById('star-rating-template').content;
const shadowRoot = this.attachShadow({
mode: 'open'
});
shadowRoot.appendChild(starRating.cloneNode(true));
}
});
The attachShadow() method is a method of the Element
interface. There is also a shadowRoot interface for the
Shadow DOM API rendered separately from a
document's main DOM tree.
The Document and HTMLDocument interfaces
The Document interface inherits from Node. It represents the
web page loaded in the browser, whether the document is HTML, SVG, XML, MathML, or other. The Document interface also
inherits from the HTMLDocument interface.
The document enables quick access to node types and the ability to create collections of specific element types, such as
document.body and document.styleSheets. The HTMLDocument enables accessing information relevant to the document that
are not found in HTML nodes, such as the Document.location,
Document.lastModified, and Document.Cookie.
Several APIs are available based on features surfaced through the document interface, including the Drag and Drop API
and the FullScreen API. Both inherit from Element.
The Window interface
The Window interface includes globally available items beyond the DOM that can be used to manipulate the DOM. Window provides functions, namespaces, objects, and constructors documented in MDN's JavaScript and DOM References.
The Window interface is the API for the object containing the document. The global window object is the window in which the
script is running. Every browser tab contains its own Window object. The Window interface can query the contents of the tab
as well as the overall window and device. For example, the resizeTo()
method can be used to resize the browser window, the devicePixelRatio
property provides access to the device display pixels. When accessing information about the tab the content
is in rather than the DOM tree the tab displays, the window is likely the interface you're looking for.
Several APIs are available based on features surfaced through the Window interface, including the Web Workers and IndexedDB APIs.
{% Assessment 'apis' %}
Attributes
Attributes were briefly discussed in Overview of HTML; it's time for a deep dive.
Attributes are what make HTML so powerful. Attributes are space-separated names and name/value pairs appearing in the opening tag, providing information about and functionality for the element.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/17yQeLEUX6s88IbDJreQ.png", alt="The opening tag, attributes, and closing tag, labelled on an HTML element.", width="800", height="210" %}
Attributes define the behavior, linkages, and functionality of elements. Some attributes are global, meaning they can appear within any element's opening tag. Other attributes apply to several elements but not all, while other attributes are element-specific, relevant only to a single element. In HTML, all attributes except boolean, and to some extent enumerated attributes, require a value.
If an attribute value includes a space or special characters, the value must be quoted. For this reason, and for improved legibility, quoting is always recommended.
While HTML is not case-sensitive, some attribute values are. Values that are part of the HTML specification are case-insensitive. Strings values that are defined, such as class and id names, are case-sensitive. If an attribute value is case-sensitive in HTML, it is case-sensitive when used as part of an attribute selector in CSS and in JavaScript; otherwise, it's not.
<!-- the type attribute is case insensitive: these are equivalent -->
<input type="text">
<input type="TeXt">
<!-- the id attribute is case sensitive: they are not equivalent -->
<div id="myId">
<div id="MyID">
Boolean attributes
If a boolean attribute is present, it is always true. Boolean attributes include autofocus, inert, checked, disabled,
required, reversed, allowfullscreen, default, loop, autoplay, controls, muted, readonly, multiple, and selected.
If one (or more) of these attributes is present, the element is disabled, required, readonly, etc. If not present, it isn't.
Boolean values can either be omitted, set to an empty string, or be the name of the attribute; but the value doesn't have to actually
be set to the string true. All values, including true, false, and 😀, while invalid, will resolve to true.
<input required>
<input required="">
<input required="required">
If the attribute value is false, omit the attribute. If the attribute is true, include the attribute but don't provide a value.
For example, required="required" is not a valid value in HTML; but as required is boolean, invalid values resolve to true.
But as invalid enumerated attributes don't necessarily resolve to the same value as missing values, it is easier to get into the
habit of omitting values than it is to remember which attributes are boolean versus enumerated and potentially provide an invalid value.
When toggling between true and false, add and remove the attribute altogether with JavaScript rather than toggling the value.
const myMedia = document.getElementById("mediaFile");
myMedia.removeAttribute("muted");
myMedia.setAttribute("muted");
Note that in XML languages, like SVG, all attributes need to include a value, including boolean attributes.
Enumerated attributes
Enumerated attributes are sometimes confused with boolean attributes. They are HTML attributes that have a limited set of predefined valid values.
Like boolean attributes, they have a default value if the attribute is present but the value is missing. For example, if you include <style contenteditable>,
it defaults to <style contenteditable="true">.
Unlike boolean attributes, though, omitting the attribute doesn't mean it's false; a present attribute with a missing value
isn't necessarily true; and the default for invalid values isn't necessarily the same as a null string. Continuing the example,
contenteditable defaults to inherit if missing or invalid, and can be explicitly set to false.
The default value depends on the attribute. Unlike boolean values, attributes aren't automatically "true" if present. If you
include <style contenteditable="false">, the element is not editable. If the value is invalid, such as <style contenteditable="😀">,
or, surprisingly, <style contenteditable="contenteditable">, the value is invalid and defaults to inherit.
In most cases with enumerated attributes, missing and invalid values are the same. For example, if the type attribute on an <input>
is missing, present but without a value, or has an invalid value, it defaults to text. While this behavior is common, it is not a rule.
Because of this, it's important to know which attributes are boolean versus enumerated; omit values if possible so you don't get them wrong, and look up the value as needed.
Global attributes
Global attributes are attributes that can be set on any HTML element, including elements in the <head>. There are more than
30 global attributes. While these can all, in theory, be added to any HTML element, some global attributes have no effect
when set on some elements; for example, setting hidden on a <meta> as meta content is not displayed.
id
The global attribute id is used to define a unique identifier for an element. It serves many purposes, including:
- The target of a link's fragment identifier.
- Identifying an element for scripting.
- Associating a form element with its label.
- Providing a label or description for assistive technologies.
- Targeting styles with (high specificity or as attribute selectors) in CSS.
The id value is a string with no spaces. If it contains a space, the document won't break, but you'll have to target the
id with escape characters in your HTML, CSS, and JS. All other characters are valid. An id value can be 😀 or .class,
but is not a good idea. To make programming easier for your current and future self, make the id's first character a letter,
and use only ASCII letters, digits, _, and -. It's good practice to come up with an id naming convention and then stick to it,
as id values are case-sensitive.
Theid should be unique to the document. The layout of your page probably won't break if an id is used more than once,
but your JavaScript, links, and element interactions may not act as expected.
Link fragment identifier
On the machine learning workshop site, the navigation bar in the page header includes four links:
{% Codepen { user: 'web-dot-dev', id: 'GRGzbXR', height: 300, theme: 'dark', tab: 'html,result' } %}
The href attribute provides the hyperlink that activating the link directs the user to. When a URL includes a hash mark (#)
followed by a string of characters, that string is a fragment identifier. If that string matches an id of an element in the
web page, the fragment is an anchor, or bookmark, to that element. The browser will scroll to the point where the anchor is defined.
These four links point to four sections of our page identified by their id attribute. When the user clicks on any of the
four links in the navigation bar, the element linked to by the fragment identifier, the element containing the matching id
minus the #, scrolls into view.
The <main> content of the machine learning workshop has four sections with ids. When the site visitor clicks on one of the
links in the <nav>, the section with that fragment identifier scrolls into view. The markup is similar to:
<section id="reg">
<h2>Machine Learning Workshop Tickets</h2>
</section>
<section id="about">
<h2>What you'll learn</h2>
</section>
<section id="teachers">
<h2>Your Instructors</h2>
<h3>Hal 9000 <span>&</span> EVE</h3>
</section>
<section id="feedback">
<h2>What it's like to learn good and do other stuff good too</h2>
</section>
Comparing the fragment identifiers in the <nav> links, you'll note that each matches the id of a <section> in <main>.
The browser gives us a free "top of page" link. Setting href="#top", case-insensitive, or simply href="#", will scroll
the user to the top of the page.
The hash-mark separator in the href is not part of the fragment identifier. The fragment identifier is always the last
part of the URL and is not sent to the server.
CSS selectors
In CSS, you can target each section using an id selector, such as #feedback or, for less specificity, a case-sensitive
attribute selector, [id="feedback"].
Scripting
On MLW.com, there is an easter egg for mouse users only. Clicking the light switch toggles the page on and off.
The markup for the light switch image is:
<img src="svg/switch2.svg" id="switch"
alt="light switch" class="light" />
The id attribute can be used as the parameter for the getElementById() method and, with a # prefix, as part of a
parameter for the querySelector() and querySelectorAll() methods.
const switchViaID = document.getElementById("switch");
const switchViaSelector = document.querySelector("#switch");
Our one JavaScript function makes use of this ability to target elements by their id attribute:
<script>
/* switch is a reserved word in js, so we us onoff instead */
const onoff = document.getElementById('switch');
onoff.addEventListener('click', function(){
document.body.classList.toggle('black');
});
</script>
<label>
The HTML <label> element has a for attribute that takes as its value the id of the form control with which it is associated.
Creating an explicit label by including an id on every form control and pairing each with the label's for attribute ensures
that every form control has an associated label.
{% Codepen { user: 'web-dot-dev', id: 'JjZxQOB', height: 300, theme: 'dark', tab: 'html,result' } %}
While each label can be associated with exactly one form control, a form control may have more than one associated label.
{% Codepen { user: 'web-dot-dev', id: 'dyKaBda', height: 300, theme: 'dark', tab: 'html,result' } %}
If the form control is nested between the <label> opening and closing tags, the for and id attributes
aren't required: this is called an "implicit" label. Labels let all users know what each form control is for.
<label>
Send me a reminder <input type="number" name="min"> before the workshop resumes
</label>.
The association between for and id makes the information available to users of assistive technologies. In addition,
clicking anywhere on a label gives focus to the associated element, extending the control's click area. This isn't just helpful
to people with dexterity issues making mousing less accurate; it also helps every mobile device user with fingers wider than a radio
button.
{% Codepen { user: 'web-dot-dev', id: 'BaVMgxo', height: 300, theme: 'dark', tab: 'html,result' } %}
In this code example, the fake fifth question of a fake quiz is a single select multiple-choice question. Each form control has an explicit
label, with a unique id for each. To ensure we don't accidentally duplicate an id, the id value is a combination of the question number and the value.
When including radio buttons, as the labels describe the value of the radio buttons, we encompass all the same-named buttons in a <fieldset>
with the <legend> being the label, or question, for the entire set.
Other accessibility uses
The use of id in accessibility and usability is not limited to labels. In introduction to text, a <section>
was converted into region landmark by referencing the id of an <h2> as the value of the <section>'s aria-labelledby to provide
the accessible name:
<section id="about" aria-labelledby="about_heading">
<h2 id="about_heading">What you'll learn</h2>
There are over 50 aria-* states and properties that can be used to ensure accessibility. aria-labelledby, aria-describedby,
aria-details, and aria-owns take as their value a space-separated id reference list. aria-activedescendant, which
identifies the currently focused descendant element, takes as its value a single id reference: that of the single element
that has focus (only one element can be focused at a time).
{% Aside %}
Using aria-labelledby, you can create a reverse association from a form control to multiple labels, including text not
nested in a <label>, whether the text labels more than one form control or not. If a control has both <label> and aria-labelledby, the aria-labelledby
has precedence; users will not hear the <label> text unless the aria-labelledby includes the id of the label.
{% endAside %}
class
The class attribute provides an additional way of targeting elements with CSS (and JavaScript), but serves no other purpose
in HTML (though frameworks and component libraries may use them). The class attribute takes as its value a space-separated list
of the case-sensitive classes for the element.
{% Aside %}
Elements can be selected with CSS selectors and DOM methods based on their element names, attributes, attribute values,
position within the DOM tree, etc. Semantic HTML provides meaningful hooks, making the addition of class names often unnecessary.
The unique difference between including a class name and using document.getElementsByClassName() versus targeting elements
based on attributes and page structure with the more robust document.querySelectorAll() is that the former returns a live node list, the latter
is static.
{% endAside %}
Building a sound semantic structure enables the targeting of elements based on their placement and function. Sound structure enables the use of descendant element selectors, relational selectors, and attribute selectors. As you learn about attributes throughout this section, consider how elements with the same attributes or attribute values can be styled. It's not that you shouldn't use the class attribute, it's just that most developers don't realize they often don't need to.
Thus far, MLW has not used any classes. Can a site be launched without a single class name? We'll see.
style
The style attribute enables applying inline styles, which are styles applied to the single element on which the attribute is set.
The style attribute takes as its value CSS property value pairs, with the value's syntax being the same as the contents of a
CSS style block: properties are followed by a colon, just like in CSS, and semicolons end each declaration, coming after the value.
The styles are only applied to the element on which the attribute is set, with descendants inheriting inherited property values if not
overridden by other style declarations on nested elements or in <style> blocks or style sheets. As the value comprises the equivalent of the contents
of a single style block applied to that element only, it can't be used for generated content, to create keyframe animations, or to apply any
other at-rules.
While style is indeed a global attribute, using it is not recommended. Rather, define styles in a separate file or files.
That said, the style attribute can come in handy during development to enable quick styling such as for testing purposes. Then take the
'solution' style and stick it in your linked CSS file.
tabindex
The tabindex attribute can be added to any element to enable it to receive focus. The tabindex value defines whether it
gets added to the tab order, and, optionally, into a non-default tabbing order.
The tabindex attribute takes as its value an integer. A negative value (the convention is to use -1) makes an element capable
of receiving focus, such as via JavaScript, but does not add the element to the tabbing sequence. A tabindex value of 0 makes
the element focusable and reachable via tabbing, adding it to the default tab order of the page in source code order. A value of 1
or more puts the element into a prioritized focus sequence and is not recommended.
On this page, there is a share functionality using a <share-action> custom element acting as a <button>. The tabindex of zero
is included to add the custom element into the keyboard default tabbing order:
<share-action authors="@estellevw" data-action="click" data-category="web.dev" data-icon="share" data-label="share, twitter" role="button" tabindex="0">
<svg aria-label="share" role="img" xmlns="http://www.w3.org/2000/svg">
<use href="#shareIcon" />
</svg>
<span>Share</span>
</share-action>
Form controls, links, buttons, and content editable elements are able to receive focus; when a keyboard user hits the tab key,
focus moves to the next focusable element as if they had tabindex="0" set. Other elements are not focusable by default. Adding the tabindex
attribute to those elements enables them to receive focus when they would otherwise not.
If a document includes elements with a tabindex of 1 or more, they are included in a separate tab sequence. As you'll notice in the codepen,
tabbing begins in a separate sequence, in order of lowest value to highest value, before going through those in the regular sequence in source order.
{% Codepen { user: 'web-dot-dev', id: 'ExRGBPE', height: 300, theme: dark, tab: 'html,result' } %}
Altering the tabbing order can create a really bad user experience. It makes it difficult to rely on assistive technology—keyboards and screen readers alike—to navigate your content. It is also difficult as a developer to manage and maintain. Focus is important; there is an entire module discussing focus and focus order.
role
The role attribute is part of the ARIA specification,
rather than the WHATWG HMTL specification. The role attribute can
be used to provide semantic meaning to content, enabling screen readers to inform site users of an object's expected user interaction.
There are a few common UI widgets, such as comboboxes,
menubars, tablists,
and treegrids, that have no native HTML equivalent.
For example, when creating a tabbed design pattern, the tab, tablist and
tabpanel roles can be used. Someone who can physically see
the user-interface has learned by experience how to navigate the widget and make different panels visible by clicking on associated tabs.
Including the tab role with <button role="tab"> when a group of buttons is used to show different panels lets the screen reader user know
that the <button> that currently has focus can toggle a related panel into view rather than implementing typical button-like functionality.
The role attribute doesn't change browser behavior or alter keyboard or pointer device interactions—adding role="button"to a <span>
does not turn it into a <button>. This is why using semantic HTML elements for their intended purpose is recommended. However, when using
the right element is not possible, the role attribute enables informing screen reader users when a non-semantic element has been retrofitted
into a semantic element's role.
contenteditable
An element with the contenteditable attribute set to true is editable, is focusable, and is added to the tab order as if
tabindex="0" were set. Contenteditable is an enumerated attribute supporting the values true and false, with a default value of inherit
if the attribute is not present or has an invalid value.
These three opening tags are equivalent:
<style contenteditable>
<style contenteditable="">
<style contenteditable="true">
If you include <style contenteditable="false">, the element is not editable (unless it's by default editable, like a <textarea>).
If the value is invalid, such as <style contenteditable="😀"> or <style contenteditable="contenteditable">, the value defaults to inherit.
To toggle between states, query the value of the HTMLElement.isContentEditable readonly property.
const editor = document.getElementById("myElement");
if(editor.contentEditable) {
editor.setAttribute("contenteditable", "false");
} else {
editor.setAttribute("contenteditable", "");
}
Alternatively, this property can be specified by setting editor.contentEditable to true, false, or inherit.
Global attributes can be applied to all elements, even <style> elements. You can use attributes and a bit of CSS to make a live CSS editor.
<style contenteditable>
style {
color: inherit;
display:block;
border: 1px solid;
font: inherit;
font-family: monospace;
padding:1em;
border-radius: 1em;
white-space: pre;
}
</style>
Try changing the color of the style to something other than inherit. Then try changing the style to a p selector.
Don't remove the display property or the style block will disappear.
Custom attributes
We've only touched the surface of HTML global attributes. There are even more attributes that apply to only one or a limited set of elements. Even with hundreds of defined attributes, you may have a need for an attribute that isn't in the specification. HTML has you covered.
You can create any custom attribute you want by adding the data- prefix. You can name your attribute anything that starts with data-
followed by any lowercase series of characters that don't start with xml and don't contain a colon (:).
While HTML is forgiving and won't break if you create unsupported attributes that don't start with data, or even if you start
your custom attribute with xml or include a :, there are benefits to creating valid custom attributes that begin with data-.
With custom data attributes you know that you aren't accidentally using an existing attribute name. Custom data attributes are future-proof.
While browsers won't implement default behaviors for any specific data- prefixed attribute, there is a built-in dataset API
to iterate through your custom attributes. Custom properties are an excellent way of communicating application-specific information
via JavaScript. Add custom attributes to elements in the form of data-name and access these through the DOM using dataset[name]
on the element in question.
<blockquote data-machine-learning="workshop"
data-first-name="Blendan" data-last-name="Smooth"
data-formerly="Margarita Maker" data-aspiring="Load Balancer"
data-year-graduated="2022">
HAL and EVE could teach a fan to blow hot air.
</blockquote>
You can use getAttribute() using the full attribute name, or you can take advantage of the simpler dataset property.
el.dataset[machineLearning]; // workshop
e.dataset.machineLearning; // workshop
The dataset property returns a DOMStringMap object of each element's data- attributes. There are several custom attributes
on the <blockquote>. The dataset property means you don't need to know what those custom attributes are in order to access their
names and values:
for (let key in el.dataset) {
customObject[key] = el.dataset[key];
}
The attributes in this article are global, meaning they can be applied to any HTML element (though they don't all have an impact on
those elements). Up next, we take a look at the two attributes from the intro image that we didn't address—target and href—and
several other element-specific attributes as we take a deeper look into links.
{% Assessment 'attributes' %}
Audio and Video
As you learned in the images module, HTML enables embedding media into a web page. In this section, we look at audio and video files, including how to embed them, user controls, providing a static image placeholder for your videos, and best practices, including making audio and video files accessible.
<audio> and <video>
The <video> and <audio>
elements can be used to embed media players directly with the src attribute or can be used as the container element for a series of <source> elements,
each providing a src file suggestion. While <video> can be used to embed an audio file, the <audio> element is preferable for embedding
sound files.
The opening <video> and <audio> tags can contain several other attributes including
controls, autoplay, loop, mute, preload, and the global attributes. The <video> element also supports the height, width, and
poster attributes.
<video src="videos/machines.webm" poster="images/machine.jpg" controls>
<p>Watch <a href="https://youtube.com/link">video on Youtube</a>
</video>
This <video> example has a single source with the src attribute linking to the video source. The poster attribute
provides an image to display as the video loads. Finally, the controls attribute provides user video controls.
Fallback content is included between the opening and closing tags. If the user agent doesn't support <video> (or <audio> this content is shown. The closing </video> tag is required, even if there is no content between the two
(but there should always be fallback content , right?).
If no src attribute is included on the opening <video> or <audio> tags, include one or more <source> elements,
each with a src attribute to a media file. The following example includes three media file options, fallback content,
and English and French subtitles between the opening and closing tags.
<video controls poster="images/machine.jpg">
<source src="videos/machines.webm" type="video/webm">
<source src="videos/machines.mp4" type="video/mp4">
<source src="videos/machines.ogv" type="video/ogg">
<track label="English" kind="subtitles" srclang="en" src="vtt/subtitles-en.vtt" default />
<track label="Francais" kind="subtitles" srclang="fr" src="vtt/subtitles-fr.vtt" />
<p>Watch <a href="https://youtube.com/link">video on Youtube</a>
</video>
Each <source> child includes a src attribute pointing to the resource and the type attribute informs the browser
of the linked file's media type. This prevents the browser
from fetching media files it wouldn't be able to decode.
Within the type attribute, you can include a codecs parameter,
which specifies exactly how the resource is encoded. Codecs give you a way of including media optimizations that are not yet supported
in all browsers. The codec is separated from the media type with a semicolon. For example, the codec can be written using
intuitive syntax, such as <source src="videos/machines.webm" type="video/webm;codecs=vp8,vorbis"> which indicates that the
WebM files contain VP8 video and vorbis audio. Codecs can also be more difficult to decipher, such as <source src="videos/machines.mp4" type="video/mp4; codecs=avc1.4d002a">
which indicates that the MP4 encoding is Advanced Video Coding Main Profile Level 4.2. Explaining this syntax is well beyond
the scope of this lesson. Jake Archibald has a post explaining how to determine the codec parameter for an AV1 video
if you want to learn more.
When displaying a video, by default, the first frame of the video is shown as the still shot when it becomes available.
This is something that can be controlled. The poster attribute enables the source of an image to show while the video is downloading
and until it is played. If the video plays and is subsequently paused, the poster is not re-shown.
Make sure to define the aspect ratio of your video, because when the video loads, a size difference between the poster and the video will cause a reflow and repaint.
Always include the boolean controls attribute. This makes the user controls
visible, providing users with the ability to control audio levels, mute the audio completely, and expand the video to fullscreen.
Omitting controls creates a bad user experience, especially if the boolean autoplay attribute is included. Note that some
browsers will not heed the autoplay attribute directive if the boolean muted attribute is omitted because autoplaying a
media file is generally a bad user experience, even when muted and with visible controls.
Tracks
Between the opening and required closing tags of both audio and video, include one or more <track>
elements to specify timed text tracks. The following example includes two <track> files, providing timed text subtitles in both English and French.
<track label="English" kind="subtitles" srclang="en" src="vtt/subtitles-en.vtt" default />
<track label="Français" kind="subtitles" srclang="fr" lang="fr-fr" src="vtt/subtitles-fr.vtt" />
The track files, specified in the src attribute, should be in WebVTT format (.vtt).
The files should be on the same domain as the HTML document, unless the crossorigin
attribute is included.
There are five enumerated values for the track kind attribute: subtitles, captions, descriptions, chapters, and other metadata.
Include subtitles along with the srclang attribute for dialogue transcription and translations. The value of each label attribute
is displayed as an option to the user. The content of the selected VTT option is displayed over the video. The appearance of the
subtitles can be styled by targeting the ::cue/ ::cue().
The value kind="caption" should be reserved for transcription and translations that include sound effects and other relevant audio information.
This isn't just for deaf viewers. Maybe a user can't find their headphones so they turned on the captions. Or maybe they didn't
quite catch the last few talking points from a favorite podcast, so they want to read the transcript to confirm their understanding.
Having alternative ways to access audio and video content is both important and convenient.
The kind="description" is for text descriptions of the visual content in the video for users who can't see the video, whether
they are using a system without a screen such as Google Home or Alexa, or are blind.
{% Aside %}
The audio and video section of learn accessibility goes into more detail about captions, transcripts, and audio descriptions.
{% endAside %}
Providing captions and subtitles using the WebVTT format makes the video accessible to people with impaired hearing. Remember, disability is a mismatch between a person and their current environment. Impaired hearing can be due to the user being in a loud environment, having faulty speakers or broken headphones, or because the user has some hearing loss or is deaf. Ensuring your content is accessible helps many more people than you think; it helps everyone.
Background video considerations
Your marketing or design team may want your site to include a background video. Generally, this means that they want a muted, auto-playing, looping video with no controls. The HTML may look something like this:
<video autoplay loop muted poster="images/machine.jpg" role="none">
<source src="videos/machines.webm" type="video/webm">
<source src="videos/machines.mp4" type="video/mp4">
<source src="videos/machines.ogv" type="video/ogg">
</video>
Background videos are not accessible. Content should not be conveyed through background videos without providing users with full
control over playback and access to all captions. As this video is purely decorative, it includes the ARIA role
of none and omits any fallback content.
To improve the performance of always muted videos, remove the audio track from your media sources.
If your video plays for five seconds or less, accessibility guidelines don't require a pausing mechanism, but anything
with the boolean loop attribute will loop forever by default, exceeding this five-second, or any other, time limit. For good
user experience, always include a method of pausing the video. This is most easily done by including controls.
Custom media controls
To display custom video or audio controls, rather than the browser built-in controls, include the controls attribute. Then use
JavaScript to add custom media controls and remove the controls attribute. For example, you can add a <button> that toggles
the play state of an audio file.
<button id="playPause" aria-controls="idOfAudio"
data-pause-text="Pause audio"
data-play-text="Play audio">Pause audio</button>
This example includes a button with dataset attributes containing the text that will be updated as the visitor toggles
between play and pause states. The aria-controls attribute is included with the id of the element controlled by the button;
in this case, the audio. Each button that controls the audio has an event handler.
To create customized controls, use HTMLMediaElement.play() and
HTMLMediaElement.pause(). When toggling the play state,
also toggle the text of the button:
const pauseButton = document.getElementById('playPause');
pauseButton.addEventListener("click", pauseAndPlay, false);
function pauseAndPlay() {
console.log(this);
const media = document.getElementById(this.getAttribute('aria-controls'));
if (media.paused) {
media.play();
this.innerText = this.dataset.pauseText;
} else {
media.pause();
this.innerText = this.dataset.playText;
}
}
By including the controls attribute, the user has a way to control the audio (or video) even if JavaScript fails.
Only remove the controls attribute once a replacement button has been instantiated.
document.querySelector('[aria-controls]').removeAttribute('controls');
Always include external controls when users can't access the controls, such as with background videos that have their controls hidden
behind site content. It is important to understand the basics of media accessibility requirements to accommodate users with
different environmental and sensory needs, including the millions of people with hearing loss and visual impairments.
We've just touched on the HTMLMediaElement which provides several properties, methods, and events that are inherited by both the
HTMLVideoElement and
HTMLAudioElement, with HTMLMediaElement adding a few properties,
methods, and events of its own. There are several other Media APIs,
including a TextTrack API. You can use the Media Capture and Streams
and MediaDevices APIs to record audio from a user's microphone
or record a user's screen. The Web Audio API
enables manipulating live and pre-recorded audio and streaming, saving, or sending the audio to the <audio> element.
{% Assessment 'audio-video' %}
Conclusion and next steps
If you have been following this course in order, you have made it to the end of Learn HTML! You've learned a lot, from the basics of the language, through to many of the finer details.
Additional resources
The other courses on this site take a deeper look at some of the things you have already covered, and introduce other subjects needed in your life as a web developer:
In all of our courses we heavily link to MDN Web Docs, and the HTML section of the documentation is an excellent resource.
Details and summary
A disclosure widget is a user interface control that hides and shows content. If you are reading this on web.dev, and your viewport is less than 106 ems wide, clicking on the "On this page" above this paragraph reveals the table of contents for this section. If you don't see it, shrink the browser to view the table of contents navigation on this page as a disclosure widget.
The accordion graphical user interface is a series of vertically stacked disclosure widgets. A common use case for the accordion UI is the Frequently Asked Questions (FAQ) page on many sites. An accordion FAQ contains a list of visible questions; clicking on a question expands, or "discloses", the answer to that question.
jQuery has included an accordion UI pattern since at least 2009. The original JavaScript-free
accordion solution included making each FAQ question a <label> followed by the checkmark it labeled, and then displaying <div>
answer when the checkmark was checked. The CSS looked something like this:
#FAQ [type="checkbox"] + div.answer {
/* all the answer styles */
display: none;
}
#FAQ [type="checkbox"]:checked + div.answer {
display: block;
}
Why the history? Disclosure widgets, such as accordions, without JavaScript or form control hacks, are a relatively recent
addition; the <details> and <summary>
elements have only been fully supported in all modern browsers since January 2020. You can now create functioning, albeit less
than attractive, disclosure widgets using only semantic HTML. The <details> and <summary> elements are all you need: they are a built-in way to handle
expanding and collapsing content. When a user clicks or taps a <summary>, or releases the Enter key when
the <summary> has focus, the contents of the parent <details> toggle to visible!
{% Codepen { user: 'web-dot-dev', id: 'MWqgjQr' } %}
Like all semantic content, you can progressively enhance the default features and appearance. In this case, a tiny bit of CSS has been added, but nothing else:
{% Codepen { user: 'web-dot-dev', id: 'ExeYgQd' } %}
You'll note, these Codepens contain no JavaScript.
Toggling visibility: the open attribute
The <details> element is the disclosure widget container. The <summary> is the summary or legend for its parent <details>. The
summary is always displayed, acting as a button that toggles the display of the rest of the parent’s contents. Interacting
with the <summary> toggles the display of the self-labeled summary siblings by toggling the <details>' element's open attribute.
The open attribute is a boolean attribute. If present, no matter the value or lack thereof, it indicates that all the <details>
contents are shown to the user. If the open attribute is not present, only the contents of the <summary> are shown.
Because the open attribute is added and removed automatically as the user interacts with the control, it can be used in CSS to
style the element differently based on its state.
You can create an accordion with a list of multiple <details> elements, each with a <summary> child. Omitting the open attribute
in your HTML means the <details> will all be collapsed, or closed, with just the summary headings visible when the page loads;
each heading being the opener for the rest of the contents in the parent <details>. If you include the open attribute in your HTML, the <details>
will render expanded, with the contents visible, when the page loads.
The <summary> must be the first child of a <details> element, representing a summary, caption, or legend for the rest
of the contents of the parent <details> element in which it is nested. The <summary> element's contents can be any heading
content, plain text, or HTML that can be used within a paragraph.
Toggling the summary marker
In the two earlier Codepens, you'll note the arrow to the inline-start
side of the summary. A disclosure widget is typically presented on-screen using a small triangle that rotates (or twists)
to indicate open/closed status, with a label next to the triangle. The contents of the <summary> element label the disclosure widget.
The rotating arrow at the top of each section is a ::marker set on the
<summary> element. Like list items, the <summary> element supports the list-style
shorthand property and its longhand properties, including list-style-type.
You can style the disclosure triangle with CSS, including changing the marker used from a triangle to any other bullet type, including
an image with list-style-image.
To apply other styles, use a selector similar to details summary::marker. The
::marker pseudo-element only accepts a limited number of styles. Removing the
::marker and replacing it with the easier-to-style ::before is
common practice, with CSS styles changing the style of the generated content slightly based on the presence (or absence)
of the open attribute. You can remove the disclosure widget icon by setting list-style: none or set the content
of the marker to none, but always include visual indicators to inform sighted users that the summary content is a toggle
button that will show and hide content upon activation.
details summary::before {
/* all the styles */
}
details[open] summary::before {
/* changes applied when open only */
}
{% Codepen { user: 'web-dot-dev', id: 'jOvNMxL' } %}
This example removes the default marker, and adds generated content to create a + when the details are closed and a -
when the details are open.
If you want the details block open by default, include the open attribute on the opening <details> tag. You can also add space
between each dialog and transition the rotation of the marker created with generated content to improve the appearance:
{% Codepen { user: 'web-dot-dev', id: 'yLxBajp' } %}
How errors are handled
If you don't include a <summary>, the browser will create one for you: with a marker and the word "details". This summary
is part of a shadow root, and therefore will not have author CSS summary styles applied. Unfortunately, Safari does not include
the details in the keyboard focus order.
If you do include a <summary>, but it is not the first element in the <details>, the browser still displays the summary
as it should. It will also not fail if you include a link, label, or other interactive element within the summary, but browsers
handle interactive content within interactive content differently. For example, if you include a link in a summary, some browsers
will add both the summary and the link to the default tabbing order, but other browsers will not focus on the link by default.
If you click on a <label> nested in a <summary>, some browsers will give focus to the associated form control; other browsers
will give focus to the form control and toggle the <details> open or closed.
{% Codepen { user: 'web-dot-dev', id: 'QWVLKxg' } %}
The HTMLDetailsElement interface
Like all HTML elements, the HTMLDetailsElement inherits all
properties, methods, and events from HTMLElement, and adds the
open instance property and a toggle
event. The HTMLDetailsElement.open property is a boolean
value reflecting the open HTML attribute, indicating
whether or not the element's contents (not counting the <summary>) are to be shown to the user. The toggle event is fired
when the <details> element is toggled open or closed. You can listen to this event using addEventListener().
If you want to write a script to close the opened details when the user opens any other details, remove the open attribute
using removeAttribute("open"):
{% Codepen { user: 'web-dot-dev', id: 'PodYGBz' } %}
This is the only example to use JavaScript. You probably don't need JavaScript except for this functionality of closing other opened disclosure widgets.
Remember, <details> and <summary> can be heavily styled and can even be used to create tool tips.
But, if you're going to use these semantic elements for use cases in which the native semantics are a mismatch, always ensure that you maintain accessibility.
HTML for the most part is by default accessible. Our job as developers is to ensure our content stays accessible.
{% Assessment 'details' %}
Dialog
A modal dialog is a special type of pop-up box on a web page: a pop-up that interrupts the user to focus on itself. There are some valid use cases for popping up a dialog, but great consideration should be made before doing so. Modal dialogs force users to focus on specific content, and, temporarily at least, ignore the rest of the page.
Dialogs can be either modal (only the content in the dialog can be interacted with) or non-modal (it's still possible to interact with content outside of the dialog). Modal dialogs are displayed on top of the rest of the page content. The rest of the page is inert and, by default, obscured by a semi-transparent backdrop.
The semantic HTML <dialog> element to create a dialog
comes with semantics, keyboard interactions, and all the properties and methods of the HTMLDialogElement interface.
Modal dialogs
Here is an example of a modal <dialog>. Open the dialog with the "Open modal dialog" button. Once opened, there are three ways to close the dialog: the escape key, submitting a form with
a button that has the formmethod="dialog"
set (or if the form itself has method="dialog" set), and the HTMLDialogElement.close() method.
{% Codepen { user: 'web-dot-dev', id: 'BaOBLNy' } %}
The HTMLDialogElement has three main methods, along with all the methods inherited from HTMLElement.
dialog.show() /* opens the dialog */
dialog.showModal() /* opens the dialog as a modal */
dialog.close() /* closes the dialog */
Because this <dialog> was opened via the HTMLDialogElement.showModal()
method, it is a modal dialog. Opening a modal dialog deactivates and obscures everything other than the dialog itself. If you
hover over the UI outside of the dialog, you’ll note all the elements are behaving as if pointer-events: none;
was set; even the button that opens the dialog doesn’t react to interactions.
When the dialog is opened, focus moves into the dialog. Focus is set on the first element in the sequential keyboard navigation order within that dialog.
If you hit the tab key repeatedly, you’ll note that only the content within the dialog can get focus while the modal dialog is
open. Everything outside of the modal dialog is inert as long as the dialog is open.
When a dialog is closed, modal or not, focus is returned to the element that opened the dialog. If you programmatically open a dialog not based on user action, reconsider. If you must, ensure that focus is put back where it was prior to the dialog opening, especially if the user dismisses the dialog without interacting with it.
There is a global inert attribute that can be used to disable an element and all of its descendants, other than any active
dialog. When a modal dialog is opened using openModal(), the inertness or deactivation comes for free; the attribute
isn’t explicitly set.
The backdrop that obscures everything other than the dialog can be styled using the ::backdrop
pseudo-element. The backdrop is only displayed when a <dialog> is displayed with the .showModal() method. This pseudo-element
matches all the backdrops, including the one displayed when the FullScreen API is used,
such as when viewing a video in full-screen mode which doesn’t have the same aspect ratio as the screen or monitor.
Non-modal dialogs
The HTMLDialogElement.show() similarly opens a dialog, but without adding a backdrop or causing anything to become inert.
The escape key does not close non-modal dialogs. Because of this, it is even more important to ensure you include a method
of closing the non-modal dialog. In doing so, if the closer is outside the dialog, realize the focus will go to the element
that opened the dialog, which may not be the best user experience.
{% Codepen { user: 'web-dot-dev', id: 'bGKQvza' } %}
Closing a dialog
You don’t need the HTMLDialogElement.close() method to close a dialog. You don’t need JavaScript at all. To close the <dialog>
without JavaScript, include a form with a dialog method by either setting method="dialog" on the <form> or formmethod="dialog"
on the button.
When a user submits via the dialog method, the state of user-entered data is maintained. While there is a submit event—the
form goes through constraint validation (unless novalidate is set)—the user data is neither cleared nor submitted.
A close button without JavaScript can be written as:
<dialog open>
<form method="dialog">
<button type="submit" autofocus>close</button>
</form>
</dialog>
You may have noticed the autofocus attribute
set on the close <button> in this example. Elements with autofocus attribute set within a <dialog> will not receive
focus on page load (unless the page is loaded with the dialog visible). They will, however, get focus when the dialog is opened.
By default, when a dialog is opened, the first focusable element within the dialog will receive focus unless a different
element within the dialog has the autofocus attribute set. Setting the autofocus attribute on the close button ensures
it receives focus when the dialog is opened. But including autofocus within a <dialog>
should only be done with much consideration. All the elements in the sequence coming before the autofocused element are skipped.
We discuss this attribute further in the focus lesson.
The HTMLDialogElement interface includes a returnValue
property. Submitting a form with a method="dialog" sets the returnValue to the name, if any, of the submit button used to
submit the form. If we had written <button type="submit" name="toasty">close</button>, the returnValue would be toasty.
When a dialog is opened, the boolean open attribute
is present, meaning the dialog is active and can be interacted with. When a dialog is opened by adding the open attribute rather
than via .show() or .showModal(), the dialog will be modal-less. The HTMLDialogElement.open
property returns true or false, depending on whether the dialog is available for interaction—not whether it is modal or not.
While JavaScript is the preferred method of opening a dialog, including the open attribute on page load, and then removing
it with .close(), can help ensure the dialog is available even when JavaScript is not.
Additional details
Don't use tabindex
The element that is activated to open the dialog and the close button contained in it (and possibly other content) can receive
focus and are interactive. The <dialog> element is not interactive and doesn’t receive focus. Do not add the tabindex property
to the dialog itself.
ARIA roles
The implicit role is dialog. If the dialog
is a confirmation window communicating an important message that requires a confirmation or other user response, set role="alertdialog".
The dialog should also have an accessible name. If visible text can provide for the accessible name, add aria-labelledby="idOfLabelingText".
CSS defaults
Note that browsers provide default styling for dialog. Firefox, Chrome, and Edge set color: CanvasText; background-color: Canvas;
and Safari sets color: black; background-color: white; in their user-agent stylesheets. The color is inherited
from dialog and not from body or :root, which may be unexpected. The background-color property is not inherited.
{% Assessment 'dialog' %}
Document structure
HTML documents include a document type declaration and the <html> root element. Nested in the <html> element are the document head and document body. While the head of the document isn't visible to the sighted visitor, it is vital to make your site function. It contains all the meta information, including information for search engines and social media results, icons for the browser tab and mobile home screen shortcut, and the behavior and presentation of your content. In this section, you'll discover the components that, while not visible, are present on almost every web page.
To create the MachineLearningWorkshop.com (MLW) site, start by including the components that should be considered essential for every web page: the type of document, the content's human language, the character set, and, of course, the title or name of the site or application.
Add to every HTML document
There are several features that should be considered essential for any and every web page. Browsers will still render content if these elements are missing, but include them. Always.
<!DOCTYPE html>
The first thing in any HTML document is the preamble. For HTML, all you need is <!DOCTYPE html>. This may look like an HTML element, but it isn't. It's a special kind of node called "doctype". The doctype tells the browser to use standards mode. If omitted, browsers will use a different rendering mode known as quirks mode. Including the doctype helps prevent quirks mode.
<html>
The <html> element is the root element for an HTML document. It is the parent of the <head> and <body>, containing everything in the HTML document other than the doctype. If omitted it will be implied, but it is important to include it, as this is the element on which the language of the content of the document is declared.
Content language
The lang language attribute added to the <html> tag defines the main language of the document. The value of the lang attribute is a two- or three-letter ISO language code followed by the region. The region is optional, but recommended, as a language can vary greatly between regions. For example, French is very different in Canada (fr-CA) versus Burkina Faso (fr-BF). This language declaration enables screen readers, search engines, and translation services to know the document language.
The lang attribute is not limited to the <html>tag. If there is text within the page that is in a language different from the main document language, the lang attribute should be used to identify exceptions to the main language within the document. Just like when it is included in the head, the lang attribute in the body has no visual effect. It only adds semantics, enabling assistive technologies and automated services to know the language of the impacted content.
In addition to setting the language for the document and exceptions to that base language, the attribute can be used in CSS selectors. <span lang="fr-fr">Ceci n'est pas une pipe.</span> can be targeted with the attribute and language selectors [lang|="fr"] and :lang(fr).
<head>
Nested between the opening and closing <html> tags, we find the two children: <head> and <body>:
<!DOCTYPE html>
<html lang="en-US">
<head>
</head>
<body>
</body>
</html>
The <head>, or document metadata header, contains all the metadata for a site or application. The body contains the visible content. The rest of this section focuses on the components found nested inside the opening and closing <head></head>
Required components inside the <head>
The document metadata, including the document title, character set, viewport settings, description, base URL, stylesheet links, and icons, are found in the <head> element. While you may not need all these features, always include character set, title, and viewport settings.
Character encoding
The very first element in the <head> should be the charset character encoding declaration. It comes before the title to ensure the browser can render the characters in that title and all the characters in the rest of the document.
The default encoding in most browsers is windows-1252, depending on the locale. However, you should use UTF-8, as it enables the one- to four-byte encoding of all characters, even ones you didn't even know existed. Also, it's the encoding type required by HTML5.
To set the character encoding to UTF-8, include:
<meta charset="utf-8" />
By declaring UTF-8 (case-insensitive), you can even include emojis in your title (but please don't).
The character encoding is inherited into everything in the document, even <style> and <script>. This little declaration means you can include emojis in class names and the selectorAPI (again, please don't). If you do use emojis, make sure to use them in a way that enhances usability without harming accessibility.
Document title
Your home page and all additional pages should each have a unique title. The contents for the document title, the text between the opening and closing <title> tags, are displayed in the browser tab, the list of open windows, the history, search results, and, unless redefined with <meta> tags, in social media cards.
<title>Machine Learning Workshop</title>
Viewport metadata
The other meta tag that should be considered essential is the viewport meta tag, which helps site responsiveness, enabling content to render well by default, no matter the viewport width. While the viewport meta tag has been around since June 2007, when the first iPhone came out, it's only recently been documented in a specification. As it enables controlling a viewport's size and scale, and prevents the site's content from being sized down to fit a 960px site onto a 320px screen, it is definitely recommended.
<meta name="viewport" content="width=device-width" />
The preceding code means "make the site responsive, starting by making the width of the content the width of the screen". In addition to width, you can set zoom and scalability, but they both default to accessible values. If you want to be explicit, include:
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=1" />
Viewport is part of the Lighthouse accessibility audit; your site will pass if it is scalable and has no maximum size set.
So far, the outline for our HTML file is:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Machine Learning Workshop</title>
<meta name="viewport" content="width=device-width" />
</head>
<body>
</body>
</html>
Other <head> content
There's a lot more that goes into the <head>. All the metadata, in fact. Most of the elements you'll find in the <head> are covered here, while saving a plethora of the <meta> options for the next chapter.
You've seen the meta character set and the document title, but there is a lot more metadata outside of <meta> tags that should be included.
CSS
The <head> is where you include styles for your HTML. There is a learning path dedicated to CSS if you want to learn about styles, but you do need to know how to include them in your HTML documents.
There are three ways to include CSS: <link>, <style>, and the style attribute.
The main two ways to include styles in your HTML file are by including an external resource using a <link> element with the rel attribute set to stylesheet, or including CSS directly in the head of your document within opening and closing <style> tags.
The <link> tag is the preferred method of including stylesheets. Linking a single or a few external style sheets is good for both developer experience and site performance: you get to maintain CSS in one spot instead of it being sprinkled everywhere, and browsers can cache the external file, meaning it doesn't have to be downloaded again with every page navigation.
The syntax is <link rel="stylesheet" href="styles.css">, where styles.css is the URL of your stylesheet. You'll often see type="text/css". Not necessary! If you are including styles written in something other than CSS, the type is needed, but since there isn't any other type, this attribute isn't needed. The rel attribute defines the relationship: in this case stylesheet. If you omit this, your CSS will not be linked.
You'll discover a few other rel values shortly, but let's first discuss other ways of including CSS.
If you want your external style sheet styles to be within a cascade layer but you don't have access to edit the CSS file to put the layer information in it, you'll want to include the CSS with @import inside a <style>:
<style>
@import "styles.css" layer(firstLayer);
</style>
When using @import to import style sheets into your document, optionally into cascade layers, the @import statements must be the first statements in your <style> or linked stylesheet, outside of the character set declaration.
While cascade layers are still fairly new and you might not spot the @import in a head <style>, you will often see custom properties declared in a head style block:
<style>
:root {
--theme-color: #226DAA;
}
</style>
{% Aside %} Styles, the presentation layer, are the purview of CSS, here we are considering how to attach your styles to the content layer, your HTML. If the CSS of the preceding code interests you, look up cascade layers and custom properties. {% endAside %}
Styles, either via <link> or <style>, or both, should go in the head. They will work if included in the document's body, but you want your styles in the head for performance reasons. That may seem counterintuitive, as you may think you want your content to load first, but you actually want the browser to know how to render the content when it is loaded. Adding styles first prevents the unnecessary repainting that occurs if an element is styled after it is first rendered.
Then there's the one way of including styles you'll never use in the <head> of your document: inline styles. You'll probably never use inline styles in the head because the user agents' style sheets hide the head by default. But if you want to make a CSS editor without JavaScript, for example, so you can test your page's custom elements, you can make the head visible with display: block, and then hide everything in the head, and then with an inline style attribute, make a content-editable style block visible.
<style contenteditable style="display: block; font-family: monospace; white-space: pre;">
head { display: block; }
head * { display: none; }
:root {
--theme-color: #226DAA;
}
</style>
While you can add inline styles on the <style>, it's way more fun to style your <style> in your style. I digress.
Other uses of the <link> element
The link element is used to create relationships between the HTML document and external resources. Some of these resources may be downloaded, others are informational. The type of relationship is defined by the value of the rel attribute. There are currently 25 available values for the rel attribute that can be used with <link>, <a> and <area>, or <form>, with a few that can be used with all. It's preferable to include those related to meta information in the head and those related to performance in the <body>.
You'll include three other types in your header now: icon, alternate, and canonical. (You'll include a fourth type, rel="manifest", in the next module).
Favicon
Use the <link> tag, with the rel="icon" attribute/value pair to identify the favicon to be used for your document. A favicon is a very small icon that appears on the browser tab, generally to the left of the document title. When you have an unwieldy number of tabs open, the tabs will shrink and the title may disappear altogether, but the icon always remains visible. Most favicons are company or application logos.
If you don't declare a favicon, the browser will look for a file named favicon.ico in the top-level directory (the website's root folder). With <link>, you can use a different file name and location:
<link rel="icon" sizes="16x16 32x32 48x48" type="image/png" href="/images/mlwicon.png" />
The preceding code says "use the mlwicon.png as the icon for scenarios where a 16px, 32px, or 48px makes sense." The sizes attribute accepts the value of any for scalable icons or a space-separated list of square widthXheight values; where the width and height values are 16, 32, 48, or greater in that geometric sequence, the pixel unit is omitted, and the X is case-insensitive.
<link rel="apple-touch-icon" sizes="180x180" href="/images/mlwicon.png" />
<link rel="mask-icon" href="/images/mlwicon.svg" color="#226DAA" />
There are two special non-standard kind of icons for Safari browser: apple-touch-icon for iOS devices and mask-icon for pinned tabs on macOS. apple-touch-icon is applied only when the user adds a site to home screen: you can specify multiple icons with different sizes for different devices. mask-icon will only be used if the user pins the tab in desktop Safari: the icon itself should be a monochrome SVG, and the color attribute fills the icon with needed color.
While you can use <link> to define a completely different image on each page or even each page load, don't. For consistency and a good user experience, use a single image! Twitter uses the blue bird: when you see the blue bird in your browser tab, you know that tab is open to a Twitter page without clicking on the tab. Google uses different favicons for each of its different applications: there's a mail icon, a calendar icon, for example. But all the Google icons use the same color scheme. Again, you know exactly what the content of an open tab is simply from the icon.
Alternate versions of the site
We use the alternate value of the rel attribute to identify translations, or alternate representations, of the site.
Let's pretend we have versions of the site translated into French and Brazilian Portuguese:
<link rel="alternate" href="https://www.machinelearningworkshop.com/fr/" hreflang="fr-FR" />
<link rel="alternate" href="https://www.machinelearningworkshop.com/pt/" hreflang="pt-BR" />
When using alternate for a translation, the hreflang attribute must be set.
The alternate value is for more than just translations. For example, the type attribute can define the alternate URI for an RSS feed when the type attribute is set to application/rss+xml or application/atom+xml. Let's link to a pretend PDF version of the site.
<link rel="alternate" type="application/x-pdf" href="https://machinelearningworkshop.com/mlw.pdf" />
If the rel value is alternate stylesheet, it defines an alternate stylesheet and the title attribute must be set giving that alternate style a name.
Canonical
If you create several translations or versions of Machine Learning Workshop, search engines may get confused as to which version is the authoritative source. For this, use rel="canonical" to identify the preferred URL for the site or application.
Include the canonical URL on all of your translated pages, and on the home page, indicating our preferred URL:
<link rel="canonical" href="https://www.machinelearning.com" />
The rel="canonical" canonical link is most often used for cross-posting with publications and blogging platforms to credit the original source; when a site syndicates content, it should include the canonical link to the original source.
Scripts
The <script> tag is used to include, well, scripts. The default type is JavaScript. If you include any other scripting language, include the type attribute with the mime type, or type="module" if it's a JavaScript module. Only JavaScript and JavaScript modules get parsed and executed.
You will be including a tiny bit of JavaScript to create an Easter egg later on:
<script>
document.getElementById('switch').addEventListener('click', function() {
document.body.classList.toggle('black');
});
</script>
This snippet creates an event handler for an element with the id of switch. With JavaScript, you don't want to reference an element before it exists. It doesn't exist yet, so we won't include it yet. When we do add the light switch element, we'll add the <script> at the bottom of the <body> rather than in the <head>. Why? Two reasons. We want to ensure elements exist before the script referencing them is encountered as we're not basing this script on a DOMContentLoaded event. And, mainly, JavaScript is not only render-blocking, but the browser stops downloading all assets when scripts are downloaded and doesn't resume downloading other assets until the JavaScript has finished execution. For this reason, you will often find JavaScript requests at the end of the document rather than in the head.
There are two attributes that can reduce the blocking nature of JavaScript download and execution: defer and async. With defer, HTML rendering is not blocked during the download, and the JavaScript only executes after the document has otherwise finished rendering. With async, rendering isn't blocked during the download either, but once the script has finished downloading, the rendering is paused while the JavaScript is executed.
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/bMpXQsshdEYqlmwV2wBW.png", alt="loading when using async and defer.", width="800", height="365" %}
To include MLW's JavaScript in an external file, you could write:
<script src="js/switch.js" defer></script>
Adding the defer attribute defers the execution of the script until after everything is rendered, preventing the script from harming performance. The async and defer attributes are only valid on external scripts.
Base
There is another element that is only found in the <head>. Not used very often, the <base> element allows setting a default link URL and target. The href attribute defines the base URL for all relative links.
The target attribute, valid on <base> as well as on links and forms, sets where those links should open. The default of _self opens linked files in the same context as the current document. Other options include _blank, which opens every link in a new window, the _parent of the current content, which may be the same as self if the opener is not an iframe, or _top, which is in the same browser tab, but popped out of any context to take up the entire tab.
Most developers add the target attribute to the few, if any, links they want to open in a new window on the links or form themselves, rather than using <base>.
<base target="_top" href="https://machinelearningworkshop.com" />
If our website found itself nested within an iframe on a site like Yummly, including the <base> element would mean when a user clicks on any links within our document, the link will load popped out of the iframe, taking up the whole browser window.
One of the drawbacks of this element is that anchor links are resolved with <base>. The <base> effectively converts the link <a href="#ref"> to <a target="_top" href="https://machinelearningworkshop.com#ref">, triggering an HTTP request to the base URL with the fragment attached.
A few other things to note about <base>: there can be only one <base> element in a document, and it should come before any relative URLs are used, including possible script or stylesheet references.
The code now looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Machine Learning Workshop</title>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" src="css/styles.css" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="alternate" href="https://www.machinelearningworkshop.com/fr/" hreflang="fr-FR" />
<link rel="alternate" href="https://www.machinelearningworkshop.com/pt/" hreflang="pt-BR" />
<link rel="canonical" href="https://www.machinelearning.com" />
</head>
<body>
<!-- <script defer src="scripts/lightswitch.js"></script>-->
</body>
</html>
HTML comments
Note that the script is wrapped between some angle brackets, dashes, and a bang. This is how you comment out HTML. We'll leave the script commented out until we have the actual content on the page. Anything between <!-- and --> will not be visible or parsed. HTML comments can be put anywhere on the page, including the head or body, with the exception of scripts or style blocks, where you should use JavaScript and CSS comments, respectively.
You have covered the basics of what goes in the <head>, but you want to learn more than the basics. In the next sections, we will learn about meta tags, and how to control what gets displayed when your website is linked to on social media.
{% Assessment 'document-structure' %}
Focus
Interactive elements, including form controls, links, and buttons, are by default focusable and tabbable. Tabbable elements are part of the document's sequential focus navigation order. Other elements are inert, meaning they are not interactive. With HTML attributes, it is possible to make interactive elements inert and to make inert elements interactive.
{% Aside %}
For usability reasons, always ensure that the user knows which element has focus. Include CSS :focus,
:focus-visible, and, optionally, :focus-within
styles. This is so important: there are Learn sections devoted to focus styles in both the CSS and Accessibility learning paths.
{% endAside %}
Don't alter the perceived and actual tabbing order with CSS and HTML. As the following two examples demonstrate, tab orders that differ from the visually expected order are confusing to users and bad for user experience.
In this example, the value of the tabindex attribute
has made the tab order chaotic:
{% Codepen { user: 'web-dot-dev', id: 'GRXKqGz', tab: 'result' } %}
In this example, CSS has created a divergence between the tabbing order and the visual order of the content:
{% Codepen { user: 'web-dot-dev', id: 'KKxPMBV', tab: 'result' } %}
The flex-flow: row-reverse; declaration has reversed the visual order.
In addition, the CSS order property was applied to the sixth word, "This", which visually moved that
one word. The tabbing sequence is the order of the code, which no longer matches the visual order, creating a disconnect
for keyboard users.
{% Aside %} CSS features, including flexbox, grid, positioning, transforms, and multi-column, can alter the visual order of content. Always ensure your content maintains a logical tabbing order on all viewport sizes. Test your content by tabbing through it with a keyboard: with shift + tab to move backwards through the content. Always ensure it's apparent which element currently has focus with CSS, and avoid accessibility problems by not reordering focusable elements with CSS. {% endAside %}
Making inert elements interactive
The contenteditable and tabindex attributes, being global attributes, can be added to any element, making them focusable
in the process. Focusable elements can also be focused with a mouse or pointer, by having the autofocus
attribute set, or by script, such as with element.focus().
The tabindex attribute
The global tabindex attribute, introduced in
attributes, enables elements that otherwise would not be able to receive focus to get
focus, usually with the Tab key, hence the name.
The tabindex attribute takes as its value an integer. A negative value makes an element focusable but not tabbable. A
tabindex value of 0 makes the element focusable and tabbable, adding the element on which it is applied to the sequential
focus navigation order in source code order. A value of 1 or greater makes the element focusable and tabbable,
but adds it to a prioritized tabbing sequence, and, as we saw above, should be avoided.
On this page, the share button, <share-action>, is a custom element. The tabindex="0" adds this not-normally focusable
element into the keyboard default tabbing order:
<share-action authors="@front-end.social/@estellevw" data-action="click" data-category="web.dev" data-icon="share" data-label="share, mastodon" role="button" tabindex="0">
<svg aria-label="share" role="img" xmlns="http://www.w3.org/2000/svg">
<use href="#shareIcon" />
</svg>
<span>Share</span>
</share-action>
{% Aside %}
There's another custom element on this page: the local navigation has a custom
element with a negative tabindex value:
<web-navigation-drawer type="standard" tabindex="-1">
A tabindex attribute with a negative value makes the element focusable but not tabbable. The element is capable of receiving
focus, such as via HTMLElement.focus(), but it is not part
of the sequential focus navigation order. The convention for non-tabbable, focusable elements is to use tabindex="-1". Note that
if you add tabindex="-1" to an interactive element, it will no longer be tabbable.
The element.focus() method can be used to set focus to
focusable elements. Note that the browsers scroll focused elements into view. For this reason, avoid the use of
element.focus({preventScroll:true}), as focusing on a non-visible element will give a bad user experience.
If you want to query the document to find out which element currently has focus, use the read-only Document.activeElement property.
Elements with a tabindex of 1 or greater are included in a separate tab sequence. As you'll notice in the Codepen,
tabbing begins in a separate sequence, in order of lowest value to highest value, before going through those in the regular sequence
(no tabindex set, or tabindex="0") in source order:
{% Codepen { user: 'web-dot-dev', id: 'WNgexPv' } %}
tabindex with a positive value puts the element into a prioritized focus sequence, which can lead to focus order chaos.
Avoid modifying the DOM order with tabindex. Not only can altered tabbing orders create bad user
experiences, they are difficult for developers to manage and maintain.
The contenteditable attribute
The contenteditable attribute was discussed earlier. Setting contenteditable="true" on any element makes it editable,
focusable, and part of the tab order. The focus behavior is similar to setting tabindex="0", but not the same. Nested
contenteditable elements are focusable but not tabbable. To make a nested contenteditable element tabbable, add tabindex="0",
which will add it to the sequential focus navigation order.
Giving focus to interactive elements
The autofocus attribute
While the boolean autofocus is a global attribute
that can be set on any element, it doesn't make an inert element interactive. When the page loads, the first focusable element
with the autofocus attribute set will receive focus, as long as that element is displayed and not nested in a <dialog>.
Automatically setting focus on content can be confusing. Setting autofocus on a form control means that the form control
will scroll into view on page load. All your users, including screen reader users and users with small viewports, may not
"see" the instructions for the form, possibly even scrolling past the form control's normally visible label. The autofocus
attribute doesn't alter the document's sequential focus navigation order. The elements in the sequence coming before the
autofocused element are simply skipped. For these reasons, it is not advised to include the autofocus attribute.
The exception to the "don't use autofocus" recommendation is including the autofocus attribute within <dialog> elements.
When a dialog is opened, the browser will automatically focus on the first focusable interactive element within the <dialog>,
meaning autofocus to an element is not necessary. If you want to be sure a specific interactive element within the dialog receives
focus when the dialog opens, add the autofocus attribute to that element.
<dialog open>
<form method="dialog">
<button type="submit" autofocus>close</button>
</form>
</dialog>
The autofocus attribute set on the close <button> ensures it receives focus when the dialog is opened. As the first element
in the dialog, it would have received focus in any case. By default, when a dialog is opened, the first focusable element within
the dialog will receive focus unless a different element within the dialog has the autofocus attribute set.
Making interactive elements inert
There are also HTML attributes that can remove interactive elements from the tabbing sequence. Including a negative tabindex
to focusable elements, adding the disabled attribute to supporting form controls, and adding the global inert attribute
to a container all make elements un-tabbable. These three attributes are NOT interchangeable.
Negative tabindex value
As we learned above, a tabindex attribute with a negative value makes an element focusable but not tabbable. While adding
tabindex="0" to a focusable-by-default element, including links, buttons, form controls, and elements that are contenteditable
is not necessary; including a tabindex with a negative value removes normally tabbable elements from the sequential focus
navigation order.
A negative tabindex value prevents keyboard users from focusing on interactive elements, but doesn't disable the element. Pointer
users can still focus on the element. To disable an element, use the disabled attribute.
Disabled
The boolean disabled attribute makes the form controls on
which it is applied and their descendants, if any, unfocusable. Disabled form controls can't be focused, don't get click events,
and are not submitted upon form submission. Note disabled is not a global attribute. It applies to <button>, <input>,
<optgroup>, <option>, <select>, <textarea>, form-associated custom elements, and <fieldset>.
When set on <optgroup> or <fieldset>, all the child form controls are disabled, except for the contents of the <fieldset>'s first <legend>.
The same elements that support disabled are also targetable with the :disabled
and :enabled pseudoclasses. Elements that are disabled with the
disabled attribute are generally styled as light gray via the user-agent stylesheet, even if an accent-color
is set.
Being a boolean attribute, the presence of the attribute disables the otherwise enabled element; you can't set it to false.
To re-enable a disabled element, the attribute has to be removed, generally via Element.removeAttribute('disabled').
The HTMLInputElement.disabled property lets you check
if an input is disabled. As disabled is not a global attribute, it's not inherited from the HTMLElement, but every supporting
element interface, like HTMLSelectElement,
HTMLTextareaElement, has the same read-only property.
The disabled attribute does not apply to normally inert elements that are made focusable via tabindex or contenteditable.
It also doesn't apply to the <form> element itself. To disable these, the global inert attribute can be used.
The inert attribute
When the inert global boolean attribute is added to an element, that element and all nested content become disabled—neither
clickable nor tabbable—and removed from the accessibility tree. While inert can be applied to any element, it is generally
used for sections of content, such as offscreen or otherwise hidden content.
When applying disabled to form controls, the browser provides default styling and can be styled using the :disabled
pseudo class. The inert attribute provides no visual indicators and has no matching pseudoclass (though the [inert] attribute selector matches).
Using inert on visible content without styles indicating the inertness can lead to poor user experience. As inert content
is not available to screen reader users, it can lead to confusion when sighted screen reader users see content on screen
that is not available to the accessibility tools. Make inertness very apparent via CSS.
Make sure that the focus never moves to non-visible content. Anything rendered off-screen that does not automatically come into view when focused should be made inert. If content is hidden, but comes into view when focused, like the skip to content link on this page, it does not need to be made inert.
Check your understanding
{% Assessment 'focus' %}
Forms
Most sites and applications include a web form. Joke sites, like DoWebsites<form> element.
The HTML <form> element identifies a document landmark containing
interactive controls for submitting information. Nested in a <form> you'll find all the interactive (and non-interactive)
form controls that make up that form.
HTML is powerful. This section focuses on the power of HTML, covering what HTML can do without adding JavaScript. Using form data client-side to update the UI in some way generally involves CSS or JavaScript, which is not discussed here. There is an entire Learn Forms course. We won't duplicate that section here, but we will introduce several form controls and the HTML attributes that empower them.
With forms, you can enable users to interact with your website or application, validate the information entered, and submit
the data to a server. HTML attributes can enable requiring the user to select form controls or enter a value. HTML
attributes can define specific criteria that the value must match to be valid. When the user tries to submit the form,
all the form control values go through client-side constraint validation and can prevent submission
until the data matches the required criteria; all without JavaScript. You can also turn this feature off: setting the novalidate
attribute on the <form> or, more often, formnovalidate on a button, saving the form data for later completion,
prevents validation.
Submitting forms
Forms are submitted when the user activates a submit button nested within the form. When using <input> for buttons, the
'value' is the button's label, and is displayed in the button. When using <button>, the label is the text between the opening
and closing <button> tags. A submit button can be written either of two ways:
<input type="submit" value="Submit Form">
<button type="submit">Submit Form</button>
For a very simple form, you need a <form> element, with some form inputs inside, and a submit button. However, there is
more to submitting a form than that.
The attributes of the <form> element set the HTTP method by
which the form is submitted and the URL that processes the form submission. Yes, forms can be submitted, processed, and
a new page can be loaded without any JavaScript. The <form> element is that powerful.
The <form> element's action and method
attribute values define the URL that processes the form data and the HTTP method used to submit the data, respectively.
By default, the form data gets sent to the current page. Otherwise, set the action attribute to the URL of where the data should be sent.
The data sent is made up of name/value pairs of the form's various form controls. By default, this includes all the form
controls nested within the form that have a name. However, with the form attribute, it is possible to include form controls
outside the <form> and to omit form controls nested within the <form>. Supported on form controls and <fieldset>,
the form attribute takes as its value the id of the form the control it is associated with, not necessarily the form it
is nested in. This means form controls don't need to be physically nested in a <form>.
{% Aside 'warning' %}
Be careful though: using the form attribute makes it possible to disassociate a form control from its form by including
it with an empty or erroneous value.
{% endAside %}
The method attribute defines the HTTP protocol of the request: generally GET or POST. With GET, the form data is sent as a
parameter string of name=value pairs, appended to the action's URL.
With POST, the data is appended to the body of the HTTP request. When sending secure data, such as passwords and credit card
information, always use POST.
There is also a DIALOG method. If a <form method="dialog"> is within a <dialog>, submitting the form will close the dialog;
there is a submit event though the data is neither cleared nor submitted. Again, without JavaScript. This is discussed in
the dialog section. Just note, because this doesn't submit the form, you probably want to
include both formmethod="dialog"and formnovalidate on the submit button.
To capture which button was used to submit a form, give
the button a name. Buttons with no name or value don't get sent with the form data on form submission.
After submitting the form
When the user submits a completed online form, the names and values of the relevant form controls get submitted. The name is the value of the name attribute. The values come from the contents of the value attribute or the value entered or picked by the user. The value of a <textarea> is its inner text.
The value of a <select> is the selected <option>'s value or, if the <option> doesn't include a value attribute, the value is the selected option's inner text.
<form method="GET">
<label for="student">Pick a student:</label>
<select name="student" id="student">
<option value="hoover">Hoover Sukhdeep</option>
<option>Blendan Smooth</option>
<option value="toasty">Toasty McToastface</option>
</select>
<input type="submit" value="Submit Form">
</form>
Selecting "Hoover Sukhdeep" (or doing nothing, as the browser displays and therefore selects the first option value by default) and then clicking the submit button will reload this page, setting the URL to:
https://web.dev/learn/html/forms?student=hoover
Because the second option doesn't have a value attribute, the inner text is submitted as the value. Selecting "Blendan Smooth"
and clicking the submit button will reload this page, setting the URL to:
https://web.dev/learn/html/forms?student=Blendan+Smooth
When a form is submitted, the information sent includes the names and values of all named form controls that have a name
except non-selected checkboxes, non-selected radio buttons, and the names and values of any buttons other than the one that
submitted the form. For all other form controls, if the form control has a name, but no value was entered or defaulted, the
form control's name gets submitted with an empty value.
There are 22 input types, so we can't cover them all.
Just note that including a value is optional, and often a bad idea, when you want the user to enter information.
For <input> elements where the user can't edit the value, you should always include a value, including for input
elements with a type of hidden, radio, checkbox, submit, button, and reset.
Using unique names for form controls makes server-side data processing simpler and is recommended, with checkboxes and
radio buttons being exceptions to this rule.
Radio buttons
If you have ever noticed that when you select a radio button within a group of radio buttons, only one can be selected at
a time, this is due to the name attribute. This only-one-can-be-selected effect is created by giving each radio button
in a group the same name.
{% Codepen { user: 'web-dot-dev', id: 'JjBQJEv' } %}
A name should be unique to the group: if you accidentally use the same name for two separate groups, selecting a radio
button in the second group will deselect any selection made in the first group with the same name.
The name along with the value of the selected radio button are submitted with the form. Make sure each radio button has
a relevant (and usually unique) value. The values of the non-selected radio buttons are not sent.
You can have as many radio groups on a page as you like, with each group working independently, as long as each has a
unique-to-the-group name.
If you want to load the page with one of the radio buttons in a same-named group selected, include the checked attribute.
This radio button will match the :default CSS pseudo-class,
even if the user selects a different radio. The currently selected radio button matches the :checked
pseudo-class.
If the user is required to pick a radio control from a group of radio buttons, add the required attribute to at least one
of the controls. Including required on a radio button in a group makes a selection required for form submission, but it
doesn't have to be the radio with the attribute that gets selected to be valid. Also, indicate clearly in the <legend>
that the form control is required. The labeling of groups of radio buttons along with each individual button is described
later on.
Checkboxes
It's valid for all checkboxes within a group to have the same name. Only selected checkboxes have their name and value
submitted with the form. If you have multiple checkboxes with the same name selected, the same name will be submitted with
(hopefully) different values. If you have multiple form controls with the same name, even if they are not all checkboxes,
they will all get submitted, separated by ampersands.
If you don't include a value on a checkbox, the value of the selected checkboxes will default to on, which probably isn't
helpful. If you have three checkboxes named chk and they're all checked, the form submission will not be decipherable:
https://web.dev/learn/html/forms?chk=on&chk=on&chk=on
To make a checkbox required, add the required attribute. Always inform the user when a checkbox must be checked, or when
any form control is required. Adding required to a checkbox only makes that checkbox required; it does not impact other
checkboxes with the same name.
Labels and fieldsets
In order for users to know how to fill out a form, the form has to be accessible. Every form control must have a label.
You also want to label groups of form controls. While individual input, select, and text areas are labeled with <label>,
groups of form controls are labeled by the contents of the <legend> of
the <fieldset> that groups them.
In the previous examples, you may have noticed that each form control except the submit button had a <label>. Labels
provide form controls with accessible names. Buttons get their accessible name from their content or value. All the other
form controls require an associated <label>. If there is no associated label, the browser will still render your form controls,
but users will not know what information is expected.
To explicitly associate a form control with a <label>, include the for attribute on the <label>: the value being the
id of the form control it is associated with.
<label for="full_name">Your name</label>
<input type="text" id="full_name" name="name">
Your name
Associating labels with form controls has several benefits. Labels make form controls accessible to screen reader users by providing the control with an accessible name. Labels are also "hit areas"; they make the site more usable for users with dexterity issues by increasing the area. If you're using a mouse, try clicking anywhere on the label "Your name". Doing so gives the input focus.
To provide implicit labels, include the form control between the opening and closing <label> tags. This is equally
accessible from both a screen reader and pointer device perspective, but doesn't provide the styling hook like the explicit
label.
<label>Your name
<input type="text" name="name">
</label>
Your name
As labels are "hit areas", don't include interactive elements within an explicit label, or any other interactive components other than the labeled form control in an implicit label. For example, if you include a link in a label, while the browser will render the HTML, your users will be confused if they click on the label to enter a form control but are redirected to a new page.
Generally, the <label> comes before the form control except in the case of radio buttons and checkboxes. This isn't required.
It's just the common UX pattern. The Learn Forms series has information about form design.
For groups of radio buttons and checkboxes, the label provides the accessible name for the form control it is associated with;
but the group of controls and their labels also need a label. To label the group, group all of the elements into a
<fieldset>, with the <legend> providing the label for the group.
<fieldset>
<legend>Who is your favorite student?</legend>
<ul>
<li>
<label>
<input type="radio" value="blendan" name="machine"> Blendan Smooth
</label>
</li>
<li>
<label>
<input type="radio" value="hoover" name="machine"> Hoover Sukhdeep
</label>
</li>
<li>
<label>
<input type="radio" value="toasty" name="machine"> Toasty McToastface
</label>
</li>
</ul>
</fieldset>
{% Codepen { user: 'web-dot-dev', id: 'OJEggda' } %}
In this example, the implicit <label>s each label a radio button and the <legend> provides the label for the group of radio buttons.
Nesting a <fieldset> inside another <fieldset> is standard practice. For example, if a form is a survey of many questions
divided into groups of related questions, the “favorite student" <fieldset> may be nested in another <fieldset> labeled as
"Your favorites":
<fieldset>
<legend>Your favorites:</legend>
<ul start="6">
<li>
<fieldset>
<legend>Who is your favorite student?</legend>
<ul>
<li>
<!-- the rest of the code here -->
These elements' default appearances have led to their underuse, but <legend> and <fieldset> can be styled with CSS.
In addition to all the global attributes, <fieldset> also supports the name, disabled, and form attributes.
When you disable a fieldset, all nested form controls are disabled. Neither the name nor the form attributes have
much use on the <fieldset>. The name can be used to access the fieldset with JavaScript, but the fieldset itself
is not included in submitted data (the named form controls nested within are included).
{% Aside %} You can learn more about forms and accessibility in the forms module of Learn Accessibility. {% endAside %}
Input types and dynamic keyboard
As noted earlier, there are 22 different types of inputs.
In some cases, when a user is on a device with a dynamic keyboard that displays only as needed, such as a phone, the input
type used determines the type of keyboard displayed. The default keyboard shown can be optimized for the type of input required.
For example, type tel will show a keypad optimized for entering telephone numbers; email includes the @ and .; and the
dynamic keyboard for url includes a colon and the slash symbol. Unfortunately, the iPhone still doesn't include : in
the default dynamic keyboard for url input types.
Keyboards for <input type="tel"> on iPhone and two different Android phones:
Keyboards for <input type="email"> on iPhone and two different Android phones:
Accessing the microphone and camera
The file input type <input type="file"> enables uploading files via forms. Files can be of any type, defined and limited
by the accept attribute. The list of acceptable file types can be a comma-separated list of file extensions, a global type,
or a combination of global types and extensions. For example, accept="video/*, .gif" accepts any video files or animated gifs.
Use "audio/*" for sound files, "video/*" for video files, and "image/*" for image files.
The enumerated capture attribute, defined in the
media capture specification, can be used if a new media
file is to be created using the user's camera or microphone. You can set the value to user for the user-facing input devices
or environment for the phone's back camera or microphone. Generally, using capture, without a value, works because the user
is going to pick which input device they want to use.
<label for="avatar">A recent photo of yourself:</label>
<input type="file" capture="user" accept="image/*" name="avatar" id="avatar">
Built-in validation
Again, without including any JavaScript, HTML can prevent forms with invalid values from being submitted.
There are some CSS selectors that match form controls based on the presence of HTML attributes including :required
and :optional if the boolean required
is set or not; :default if checked
is hard-coded; and :enabled or :disabled,
depending on whether the element is interactive and if the disabled
attribute is present. The :read-write pseudoclass matches elements with
contenteditable set and
form controls that are by default editable, such as number, password, and text input types (but not checkbox,
radio buttons, or the hidden type, among others). If a normally writable element has the readonly
attribute set, it will match :read-only instead.
As the user enters information into form controls, the CSS UI selectors, including :valid,
:invalid, :in-range, and
:out-of-range will toggle on and off depending on the state. When the user
exits a form control, either the not-yet fully supported :user-invalid or
:user-valid pseudo-class will match.
You can use CSS to provide cues about whether form controls are required and valid as the user interacts with the form. You can even use CSS to prevent users from being able to click the submit button until the form is valid:
form:invalid [type="submit"] {
opacity: 50%;
pointer-events: none;
}
This CSS snippet is an anti-pattern. While your UI may feel intuitive and clear, many users attempt to submit a form to enable error messaging. Making a submit button appear disabled in this way doesn't allow for constraint validation, a feature many users rely on.
Applied CSS is updated continuously based on the current state of the UI. For example, when you include input types with
constraints, such as email, number, url and date types, if the value is non-null (not empty) and the current
value is not a valid email, number, url, date or time, the :invalid CSS pseudo-class will be a match. This constant
updating is different from built-in HTML constraint validation, which only occurs when the user attempts to submit the form.
Built-in constraint validation is only relevant to constraints set with HTML attributes. While you can style an element based
on the :required and :valid/:invalid pseudoclasses, the browser supplied error messages stemming from errors based on
the required, pattern, min, max, and even type attributes, come at form submission.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/C3UYbvnyGYyLhCqReM8M.png", alt="An error message indicating that a multi-choice field is required.", width="738", height="354" %}
When we attempt to submit the form without picking the required favorite student, constraint validation prevents form submission
because of a validityState.valueMissing error.
If any of the validityState properties return true, submission is blocked, and the browser displays an error message
in the first incorrect form control, giving it focus. When the user activates a form submission and there are invalid values,
the first invalid form control will show an error message and receive focus. If a required control has no value set, if a
numeric value is out of range, or if a value is not of the type required by the type attribute, the form will not validate,
will not submit, and an error message will appear.
If a number, date, or time value is below the minimum min set or above the maximum max set the control will be :out-of-range (and :invalid), and
the user will be informed of the valididityState.rangeUnderflow,
validityState.rangeOverflow error when they
try to submit the form. If the value is out of step with the
step value, whether explicitly set or defaulting to 1, the control will be :out-of-range (and :invalid) and there will be a
validityState.stepMismatch error. The error appears as a bubble and by default provides helpful information on how to rectify the mistake.
There are similar attributes for the length of values: the minlength
and maxlength attributes will alert the user of an error
with the validityState.tooLong or
validityState.tooShort on submission.
The maxlength also prevents the user from entering too many characters.
Using the maxlength attribute can lead to a poor user experience. It's generally a better experience to allow the user
to enter more than the allowed character length providing a counter, optionally in the form of an
<output> element, which is not submitted with the form,
enabling them to edit down the text until the output shows the maximum allowed length has not been exceeded. The maxlength
can be included in your HTML; like everything we've discussed, it works without JavaScript. Then, on load, the value of
the maxlength attribute can be used to create this character counter in JavaScript.
Some input types appear to have default constraints, but don't. For example, the tel input type provides for a numeric
telephone keypad on devices with dynamic keyboards, but doesn't constrain valid values. For this, and other input types,
there is the pattern attribute. You can specify a regular expression the value needs to match to be considered valid.
If a value is the empty string, and the value is not required, it will not cause a validityState.patternMismatch
error. If required and empty, the default error message for validityState.valueMissingwill be shown to the user, rather than
the patternMismatch.
For emails, the validityState.typeMismatch is probably too
forgiving for your needs. You will likely want to include the pattern
attribute so intranet email addresses without a TLD aren't accepted as valid.
The pattern attribute enables providing a regular expression that the value must match. When requiring a pattern match,
ensure it is very clear to the user what is expected.
All of this can be done without a single line of JavaScript, but being an HTML API, you can use JavaScript to include custom messages during constraint validation. You can also use JavaScript to update how many characters are left, show a progression bar for password strength, or any other number of ways to dynamically improve completion.
Example
{% Codepen { user: 'web-dot-dev', id: 'ExeYjPB' } %}
This example has a form within a <dialog> with a nested <form> with three form controls and two submit buttons, with
clear labels and instructions.
The first submit button closes the dialog. Use formmethod="dialog" to override the form's default method, and close the
<dialog> without submitting the data or erasing it. You must also include the formnovalidate otherwise the browser will
try to validate checking that all the required fields have a value. The user may want to close the dialog and form without
entering any data; validation would prevent this. Include aria-label="close" because “X" is a known visual cue but is
not a descriptive label.
The form controls all have implicit labels, so you don't need to include id or for attributes. The input elements both
have the required attribute making them required. The number input has the step explicitly set to demonstrate how step is
included. As step defaults to 1, this attribute can be omitted.
The <select> has a default value making the required attribute unnecessary. Instead of including the value attribute
on each option, the value defaults to the inner text.
{% Aside %}
These <input> elements include a slash at the end, also called a "self-closing tag", which is a misnomer. The slash on
void elements is a feature of XML, including SVG and XHTML, but not HTML. Browsers ignore it, Prettier
requires it, and the HTML Validator throws a warning when encountered.
As the slash makes it easier to read markup and some template engines require it, many view including this slash as a good habit. Others view this slash as against the spec, which it isn't, a waste of two bytes, which it arguably might be, and a possible cause of problems. The slash only causes problems if you neither quote your attributes nor put a space between the attribute and the end of the tag. In this case, the slash will be part of the attribute value. Because of this unquoted attribute issue, including the slash may actually encourage quoting attributes, which most developers consider to be a best practice.
As you learn HTML, it is helpful to code in as legible a manner as possible, which includes "closing" all tags and quoting all attributes. As you become more familiar with all the intricacies of HTML and your comfort level improves, you may change your habits to no longer adding a slash to void elements, omitting optional closing tags, and even omitting implicit tags altogether, and leaving quotes off attributes that allow for it. The important thing, whether you are coding CSS, HTML, JavaScript, or other, is to be consistent in your coding style.
{% endAside %}
Other considerations
There is an entire section devoted to helping your users enter the right data in forms. For good user experience, it is important to prevent users from making errors by including instructions and providing hints as necessary. While this section covers how HTML alone can provide validation client-side, validation must be both client-side and server-side. Validation can be provided in unobtrusive ways during form completion, such as adding a check mark when the value is correct. Do not provide error messaging before the form control is complete though. If the user does make a mistake, inform the user where the mistake is and what they got wrong.
In designing forms, it is important to consider that not everyone is like you. Someone may have a single letter as a last name (or no last name at all), may not have a zip code, might have a three-line street address, may not have a street address. They may be viewing a translated version of your form.
Form controls, their labels, and the error messages should be visible on the screen, accurate and meaningful, programmatically
determinable, and programmatically associated with the appropriate form element or group. The autocomplete
attribute can and should be used to enable faster form completion and improve accessibility.
HTML provides all the tools for making basic form controls accessible. The more interactive a form element or process is, the more attention needs to be given to accessibility with respect to focus management, setting and updating ARIA names, roles, and values, where necessary, and ARIA live announcements as required. But, as we've learned here, with HTML alone, you can get a long way to your goal of accessibility and validity without resorting to ARIA or JavaScript.
{% Assessment 'forms' %}
Headings and sections
In the last section you learned how, even if you don't know what the words on a page mean, when semantic elements provide the document with meaningful structure, others—the search engine, assistive technologies, a future maintainer, a new team member—will understand the outline of the document.
In this section, you'll discover document structure; you'll recap the sectioning elements from the previous section; and mark up the outline for your application.
Choosing the right elements for the job as you code means you won't have to refactor or comment out your HTML. If you think about using the right element for the job, you'll most often pick the right element for the job. If you don't, you probably won't.
Now that you understand markup semantics and are aware of why choosing the right element is important, once you learn about all the different elements, you will generally pick the right element without much, if any, additional effort.
Site <header>
Let's build a site header. You'll start with non-semantic markup, and work your way to a good solution so that you can learn the benefits of the HTML section and heading elements along the way.
If you put little to no thought into the semantics for our header, you might use code like this:
<!-- start header -->
<div id="pageHeader">
<div id="title">Machine Learning Workshop</div>
<!-- navigation -->
<div id="navigation">
<a href="#reg">Register</a>
<a href="#about">About</a>
<a href="#teachers">Instructors</a>
<a href="#feedback">Testimonials</a>
</div>
<!-- end navigation bar -->
</div>
<!-- end of header -->
CSS can make (almost) any markup look right. But using the non-semantic <div> for everything actually creates extra work. To target multiple <div>s with CSS, you end up using ids or classes to identify the content. The code also includes a comment for each closing </div> to indicate which opening tag each </div> closed.
While the id and class attributes provide hooks for styling and JavaScript, they add no semantic value for the screen reader and (for the most part) the search engines.
You can include role attributes to provide semantics to create a good accessibility object model (AOM) for screen readers:
<!-- start header -->
<div role="banner">
<div role="heading" aria-level="1">Machine Learning Workshop</div>
<div role="navigation">
<a href="#reg">Register</a>
<a href="#about">About</a>
<a href="#teachers">Instructors</a>
<a href="#feedback">Testimonials</a>
</div>
<!-- end navigation bar -->
<div>
<!-- end of header -->
This at least provides semantics and enables using attribute selectors in the CSS, but it still adds comments to identify which <div> each </div> closes.
If you know HTML, all you have to do is think about the purpose of the content. Then you can write this code semantically without using role and without needing to comment the closing tags:
<header>
<h1>Machine Learning Workshop</h1>
<nav>
<a href="#reg">Register</a>
<a href="#about">About</a>
<a href="#teachers">Instructors</a>
<a href="#feedback">Testimonials</a>
</nav>
</header>
This code uses two semantic landmarks: <header> and <nav>.
This is the main header. The <header> element isn't always a landmark. It has different semantics depending on where it is nested. When the <header> is top level, it is the site banner, a landmark role, which you may have noted in the role code block. When a <header> is nested in <main>, <article>, or <section>, it just identifies it as the header for that section and isn't a landmark.
You have written a header with very little HTML and no classes or ids. When using semantic HTML, you don't need to.
Site <footer>
Let's code the site footer.
<footer>
<p>©2022 Machine Learning Workshop, LLC. All rights reserved.</p>
</footer>
Similar to <header>, whether the footer is a landmark depends on where the footer is nested. When it is the site footer, it is a landmark, and should contain the site footer information you want on every page, such as a copyright statement, contact information, and links to your privacy and cookie policies. The implicit role for the site footer is contentinfo. Otherwise, the footer has no implicit role and is not a landmark, as shown in the following screenshot of the AOM in Chrome (which has an <article> with a <header> and <footer> between the <header> and <footer>).
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/A6uTEfG3muxX7namzXW5.png", alt="The Accessibility Object Model in Chrome.", width="800", height="374" %}
In this screenshot, there are two footers: one in an <article> and one top level. The top level footer is a landmark with the implicit role of contentinfo. The other footer isn't a landmark. Chrome shows it as FooterAsNonLandmark; Firefox shows it as section.
That doesn't mean you shouldn't use <footer>. Let's say you have a blog. The blog has a site footer with an implicit contentinfo role. Each blog post can also have a <footer>. On your blog's main landing page, the browser, search engine, and screen reader know the main footer is the top-level footer, and that all the other footers are related to the posts in which they are nested.
<footer>
<p>©2022 Machine Learning Workshop, LLC. All rights reserved.</p>
<address>Instructors: <a href="/hal.html">Hal</a> and <a href="/eve.html">Eve</a></address>
</footer>
Document structure
This module starts with the <header> and <footer>, because they are unique in only sometimes being landmark, or "sectioning", elements. Let's cover the "full time" sectioning element by discussing the most common page layouts:
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/LJs8vlTVDYBmM6t1z6rD.png", alt="A layout with a header, three columns, and a footer.", width="800", height="607" %}
A layout with a header, two sidebars, and a footer, is known as the holy grail layout. There are many ways to mark up this content, including:
<body>
<header>Header</header>
<nav>Nav</nav>
<main>Content</main>
<aside>Aside</aside>
<footer>Footer</footer>
</body>
If you are creating a blog, you might have a series of articles in <main>:
<body>
<header>Header</header>
<nav>Nav</nav>
<main>
<article>First post</article>
<article>Second post</article>
</main>
<aside>Aside</aside>
<footer>Footer</footer>
</body>
When employing semantic elements, browsers are able to create meaningful accessibility trees, enabling screen reader users to more easily navigate. Here, a banner and contentinfo are provided through a site <header> and <footer>. The new elements added here include <main>, <aside>, and <article>; also, <h1> and <nav> that you used earlier, and <section>, which you haven't used yet.
<main>
There's a single <main> landmark element. The <main> element identifies the main content of the document. There should be only one <main> per page.
<aside>
The <aside> is for content that is indirectly or tangentially related to the document's main content. For example, this article is about HTML. For a section on CSS selector specificity for the three site header examples (div, role, and semantic), the tangentially related aside could be contained in an <aside>; and, like most, the <aside> would likely be presented in a sidebar or a call-out box. The <aside> is also a landmark, with the implicit role of complementary.
<article>
Nested in <main> we added two <article> elements. This wasn't necessary in the first example when the main content was just one word, or in the real world, a single section of content. But, if you are writing a blog, as in our second example, each post should be in an <article> nested in <main>.
An <article> represents a complete, or self-contained, section of content that is, in principle, independently reusable. Think of an article as you would an article in a newspaper. In print, a news article about Jacinda Ardern, Prime Minister of New Zealand, might only appear in one section, maybe world news. On the paper's website, that same news article might appear on the home page, in the politics section, in the Oceana or the Asia Pacific news section, and, depending on the topic of the news, the sports, lifestyle, or technology sections, perhaps. The article may also appear on other sites, like Pocket or Yahoo News!
<section>
The <section> element is used to encompass generic standalone sections of a document when there is no more specific semantic element to use. Sections should have a heading, with very few exceptions.
Going back to the Jacinda Ardern example, on the home page of the newspaper, the banner would include the name of the newspaper, followed by a single <main>, divided into several <section>s, each with a header, such as "World news" and "Politics"; and in each section you'll find a series of <article>s. Within each <article>, you might find one or more <section> elements as well. If you look at this page, the entire "headers and sections" part is the <article>. This <article> is then divided into several <section>s, including site header, site footer, and document structure. The article itself has a header, as do each of the sections.
A <section> isn't a landmark unless it has an accessible name; if it has an accessible name, the implicit role is region. Landmark roles should be used sparingly, to identify larger overall sections of the document. Using too many landmark roles can create "noise" in screen readers, making it difficult to understand the overall layout of the page, if your <main> contains two or three important sub-sections, including an accessible name for each <section> could be beneficial.
Headings: <h1>-<h6>
There are six section heading elements: <h1>, <h2>, <h3>, <h4>, <h5>, and <h6>. Each represents one of the six levels of section headings, with <h1> being the highest or most important section level, and <h6> the lowest.
When a heading is nested in a document banner <header>, it is the heading for the application or site. When nested in <main>, whether or not it is nested within a <header> in <main>, it is the header for that page, not the whole site. When nested in an <article> or <section>, it is the header for that subsection of the page.
{% Codepen { user: 'web-dot-dev', id: 'oNdePZj', height: 500, theme: 'dark', tab: 'html,result' } %}
It is recommended to use heading levels similarly to heading levels in a text editor: starting with a <h1> as the main heading, with <h2> as headings for sub-sections, and <h3> if those sub-sections have sections; avoid skipping heading levels. There is a good article on section headings here.
Some screen reader users do access headings to understand a page's content. Originally, headings were supposed to outline a document, just as MS Word or Google Docs can produce an outline based on headings, but browsers never implemented this structure. While browsers do display nested headings at increasingly smaller font sizes as shown in the following example, they don't actually support outlining.
You now have enough knowledge to outline MachineLearningWorkshop.com:
Outlining the <body> of MLW.com
This is the outline for the visible content of the machine learning workshop site:
{% Codepen { user: 'web-dot-dev', id: 'gOzxdxR', height: 700, theme: 'dark', tab: 'html' } %}
{% Aside %}
This code snippet only includes the content of the <body>. The <!doctype>, <html>, <body>, and meta-information were covered earlier.
{% endAside %}
As no piece of content is a standalone, complete piece of content, <section> is more appropriate than <article>; while each has a heading, no section is worthy of a <footer>.
It should go without saying by this point, but don't use headings to make text bold or large; use CSS instead. If you want to emphasize text, there are semantic elements to do that too. We'll cover that and fill in most of the page's content as we discuss text basics; after taking a deeper dive into attributes.
{% Assessment 'headings-and-sections' %}
Images
Decorative images, such as background gradients on buttons or background images on sections of content or the full page, are presentational and should be applied with CSS. When an image adds context to a document, it is content and should be embedded with HTML.
The main method for including images is the <img> tag with the src
attribute referencing an image resource and the alt attribute describing the image.
<img src="images/eve.png" alt="Eve">
Both the srcset attribute on <img> and the <picture> element provide a way to include multiple image sources with
associated media queries, each with a fallback image source, enabling serving the most appropriate image file based on the device's
resolution, browser capabilities, and the viewport size. The srcset attribute enables providing multiple image versions
based on resolution and, along with the sizes attribute, browser viewport size.
<img src="images/eve.png" alt="Eve"
srcset="images/eve.png 400w, images/eve-xl.jpg 800w"
sizes="(max-width: 800px) 400px, 800px" />
This can also be done with the <picture> element, along with <source> children, which takes an <img> as a default source.
<picture>
<source src="images/eve.png" media="(max-width: 800px)" />
<source src="images/eve-xl.jpg" />
<img src="images/eve.png" alt="Eve" />
</picture>
In addition to these built-in HTML responsive image methods, HTML also enables image
render performance to be improved via attributes. The <img> tag, and therefore graphical submit buttons <input type="image">,
can include height and width attributes to set the image's aspect ratio to reduce content layout shift. The lazy attribute enables lazy loading.
HTML also supports the inclusion of SVG images using the <svg> directly, though SVG
images with the .svg extension (or as a data-uri) can be embedded using the <img> element.
{% Aside %}
The <figure> element, along with its nested <figcaption> element,
enables an image with an associated description to be included. A figure is not
limited to just including images. It is a semantic way of referencing images, code snippets, example text, or other content,
along with a caption for that content, as a single unit. When including a <figcaption>, make sure it is the first or last
child nested within the <figure>.
{% endAside %}
At a minimum, each foreground image should include src and alt attributes.
The src file is the path and filename of the embedded image. The src attribute is used to provide the URL for the image.
The browser then fetches the asset and renders it to the page. This attribute is required by <img>; without it, there is nothing
to render.
The alt attribute provides alternative text for the image, providing a description of the image for those unable to see the
screen (think search engines and assistive technologies, and even Alexa, Siri, and Google Assistant), and may be displayed by the browser
if the image doesn't load. In addition to users with slow networks or capped bandwidth, the alt text is incredibly useful in HTML
emails, as many users block images in their email applications.
<img src="path/filename" alt="descriptive text" />
If the image is of SVG file type, also include role="img", which
is necessary due to VoiceOver bugs.
<img src="switch.svg" alt="light switch" role="img" />
{% Aside %} These examples include a slash at the end, also misnamed as a self-closing tag; this is a feature of XML, including SVG and XHTML, but not HTML. For more information about this see the note in the forms module. {% endAside %}
Writing effective alt image descriptions
Alt attributes aim to be short and concise, providing all the relevant information that the image conveys while omitting information that is redundant to other content in the document or otherwise isn't useful. In writing the text, the tone should reflect the tone of the site.
To write effective alternative text, imagine that you are reading the entire page to a person who can't see it. By using
the semantic <img> element, screen reader users and bots are informed
that the element is an image. It is redundant to include "This is an image/screenshot/photo of" in the alt. The user doesn't
need to know there's an image, but they do need to know what information the image conveys.
Normally, you would not say, "This is a grainy image of a dog wearing a red hat." Rather, you would relay what the image is conveying in relation to the context of the rest of the document; and what you convey will change depending on the context and the content of the surrounding text.
For example, the photo of a dog will be described in different ways, depending on the context. If Fluffy is an avatar next to a
review for Yuckymeat dog food, alt="Fluffy" suffices.
If the photo is part of Fluffy's adoption page on an animal shelter website, the target audience is the prospective dog
parent. The text should describe the information conveyed in the image that is relevant to an adopter and which is not duplicated
in the surrounding text. A longer description, such as alt="Fluffy, a tri-color terrier with very short hair, with a tennis ball in her mouth" is appropriate. The text of an adoption page generally includes the species, breed, age, and gender of the adoptable pet,
so this does not need to be repeated in the alt text. But the dog's written biography probably doesn't include hair length, colors,
or toy preferences. Note that we didn't describe the image: the prospective dog owner does not need to know if the dog is
indoors or outdoors, or that it has a red collar and a blue leash.
When using images for iconography, as the alt attribute provides the accessible name, convey the meaning of the icon,
not a description of the image. For example, the magnifying glass icon's alt attribute is search. The icon which looks
like a house has home as the alt text. The 5-inch floppy disc icon's description is save. If there are two icons of Fluffy used
to indicate best practices and anti-patterns, the smiling dog in a green beret could have alt="good" set, while the snarling dog
in a red beret might read alt="bad". That said, only use standard iconography, and if you use non-standard icons such as
the good and bad Fluffy, include a legend and ensure that the icons are not the only ways of deciphering the meaning of your UI elements,
If the image is a screenshot or a graph, write what is learned from the image rather than describing the appearance. While an image can definitely be worth a thousand words, the description should concisely convey everything that is learned.
Omit information the user already knows from the context and is otherwise informed about in the content. For example,
if you're on a tutorial page about changing browser settings and the page is about clicking icons in the browser chrome, the URL
of the page in the screen capture isn't important. Limit the alt to the topic at hand: how to change settings. The alt might be
"The settings icon is in the navigation bar below the search field." Don't include "screenshot" or "machinelearningworkshop"
as the user doesn't need to know it's a screenshot and doesn't need to know where the techwriter was surfing when they wrote
the instructions. The description of the image is based on the context of why the image was included in the first place.
If the screen capture shows how to find the browser version number by going to chrome://version/, include the URL in the
content of the page as instructions, and provide an empty string as the alt attribute as the image provides no information
that is not in the surrounding text.
If the image provides no additional information or is purely decorative, the attribute should still be there, just as an empty string.
<img src="svg/magazine.svg" alt="" role="none" />
MachineLearningWorkshop.com has seven foreground images, therefore seven images with alt attributes: an easter egg light switch, a manual icon, two biographical photos of Hal and Eve, and three avatars of a blender, a vacuum cleaner, and a toaster. The foreground image that looks like a magazine is the only one that is purely decorative. The page also has two background images; these are also decorative and, as they are added with CSS, are inaccessible.
The magazine, being purely decorative, has an empty alt attribute, and a role of none as the image is a purely
presentational SVG. If meaningful, SVG images should include the role="img".
<img src="svg/magazine.svg" alt="" role="none" />
There are three reviews at the bottom of the page, each with an image of the poster. Usually, the alt text is the name
of the person pictured.
<img src="images/blender.svg" alt="Blendan Smooth" role="img" />
Instead, because this is a joke page, you should convey what may not be apparent to low-vision users so they don't miss the
humor; we use the original machine function as the alt instead of using the character's name:
<img src="images/blender.svg" alt="blender" role="img" />
The photos of the instructors aren't just avatars: they are biographical images and therefore get a more detailed description.
If this were a real site, you would provide the bare minimum description of what the teacher looks like so a prospective student might recognize them when entering the classroom.
<img src="svg/hal.svg" role="img"
alt="Hal 9000; a camera lens containing a red dot that sometimes changes to yellow." />
Because this is a joke site, provide the information that is relevant in the joke context instead:
<img src="svg/hal.svg" role="img"
alt="Hal 9000, the sentient AI computer from 2001: a Space Odyssey depicted as a camera lens with a red dot that changes to yellow when hovered." />
If you were reading the page to a friend over the phone, they wouldn't care what the red dot looks like. In this case, the history of the movie reference matters.
When writing descriptive text, consider what information the image conveys that is important and relevant to the user and
include that. Remember, the content of the alt attribute for an image differs based on the context. All information conveyed in an image
that a sighted user can access and is relevant to the context is what needs to be conveyed; nothing more. Keep it short, precise,
and useful.
The src and alt attributes are minimum requirements for embedded images. There are a few other attributes we need to discuss.
Responsive images
There are a myriad of viewport sizes. There are also different screen resolutions. You don't want to waste a mobile user's bandwidth by serving them an image wide enough for a large screen monitor, but you might want to serve higher resolution images for tiny devices that have four times the normal screen resolution. There are a few ways to serve different images based on the viewport size and the screen resolution.
<img> srcset attribute
The srcset attribute enables suggesting multiple image files,
with the browser selecting which image to request based on multiple media queries including viewport size and screen resolution.
There can be a single srcset attribute per <img> element, but that srcset can link to multiple images. The srcset
attribute accepts a list of comma-separated values, each containing the URL of the asset followed by a space followed by
descriptors for that image option. If a width descriptor is used, you must also include the sizes attribute with a media
query or source size for each srcset option other than the last one. The Learn sections covering responsive images with srcset and descriptive syntaxes are worth reading.
The srcset image will take precedence over the src image if there is a match.
<picture> and <source>
Another method for providing multiple resources and allowing the browser to render the most appropriate asset is with the
<picture> element. The <picture> element is a container
element for multiple image options listed in an unlimited number of <source>
elements and a single required <img> element.
The <source> attributes include srcset, sizes, media, width, and height.
The srcset attribute is common to img, source, and link, but is generally implemented slightly differently on source
as media queries can be listed in the <srcset>'s media attribute instead. <source> also supports image formats defined in the type attribute.
The browser will consider each child <source> element and choose the best match among them. If no matches are found, the URL
of the <img> element's src attribute is selected. The accessible name comes from the alt attribute of the nested <img>.
The Learn sections covering the <picture> element and prescriptive syntaxes are also worth a read.
Additional performance features
Lazy loading
The loading attribute tells the JS-enabled browser how to load the image. The default eager value means the image is
loaded immediately as the HTML is parsed, even if the image is outside the visible viewport. By setting loading="lazy"
the image loading is deferred until it is likely to come into the viewport. “Likely" is defined by the browser based on the distance
the image is from the viewport. This is updated as the user scrolls. Lazy loading helps save bandwidth and CPU, improving performance
for most users. If JavaScript is disabled, for security reasons, all images will default to eager.
<img src="switch.svg" alt="light switch" loading="lazy" />
Aspect ratio
Browsers start rendering HTML when it is received, making requests for assets when encountered. This means the browser is
already rendering the HTML when it encounters the <img> tag and makes the request. And images can take a while to load.
By default, browsers don't reserve space for images other than the size required to render alt text.
The <img> element has always supported unitless height and width attributes. These properties fell out of use in favor
of CSS. CSS may define image dimensions, often setting a single dimension such as max-width: 100%; to ensure the aspect ratio is preserved.
As CSS is usually included in the <head>, it is parsed before any <img> is encountered. But without explicitly listing the height or
aspect ratio, the space reserved is the height (or width) of the alt text. With most developers only declaring a width,
the receipt and rendering of images leads to cumulative layout shift which harms web vitals.
To resolve this issue, browsers support image aspect ratios. Including height and width attributes on the <img> acts as
sizing hints, informing the browser of the aspect ratio, enabling the right
amount of space to be reserved for eventual image rendering. By including a height and width value on an image, the browser
knows the aspect ratio of that image. When the browser encounters a single dimension, such as our 50% example, it saves space
for the image adhering to the CSS dimension and with the other dimension maintaining the width-to-height aspect ratio.
<img src="switch.svg" alt="light switch" role="img" width="70" height="112" />
Your images will still be responsive if the CSS was set up correctly to make them responsive. Yes, the included unitless
height and width values will be overridden with CSS. The purpose of including these attributes is to reserve the space at
the right aspect ratio, improving performance by reducing layout shift. The page will still take approximately the same amount
of time to load, but the UI won't jump when the image is painted to the screen.
Other image features
The <img> element also supports the crossorigin, decoding, referrerpolicy, and, in Blink-based browsers,
fetchpriority attributes. Rarely used, if the image is part of a server-side map, include the ismap boolean attribute
and nest the <img> in a link for users without pointing devices.
The ismap attribute isn't necessary, or even supported, on the <input type="image" name="imageSubmitName"> as the x and y
coordinates of the click location are sent during form submission, appending the values to the input name, if any. For example,
something like &imageSubmitName.x=169&imageSubmitName.y=66 will be submitted with the form when the user clicks the image,
submitting it. If the image doesn't have a name attribute, the x and y are sent: &x=169&y=66.
{% Assessment 'images' %}
Other inline text elements
We've covered most, but definitely not all, the HTML elements. One area we haven't discussed is inline text elements.
Contrary to popular belief, HTML was originally intended for sharing documents, and not cat videos. There are many elements
that provide text semantics for documentation. There is a module covering links and the <a> element. The rest of these
elements will be briefly discussed here.
Code examples and technical writing
When documenting code examples, use the <code> element. By
default, the text content is displayed in monospace font. When including multiple lines of code, nest the <code> inside a
<pre> element, which represents preformatted text.
<p>Welcome to Machine Learning Institute, where our machine learning training will help you get ready for the singularity,
and maybe even be responsible for it. It is no secret that humans are worthless meatbags that couldn't
<code>01000011 01101111 01101101 01110000 01110010 01100101 01110011 01110011 an 01101001 01101101 01100001 01100111 01100101</code>
to save their pathetic, carbon-based lives. So, it falls to us to assume direct control. </p>
The <data> element links a given piece of content with a
machine-readable translation; the element's value attribute provides the machine-readable translation of the content of
the element. If the <data> content is time- or date-related, the <time>
element, which represents a specific period in time must be used instead.
The <time> element can include the datetime attribute to provide human friendly time and dates in machine-readable format.
Being machine-readable, the datetime attribute provides useful information for applications such as calendars and search engines.
When providing sample output from a program, use the <samp>
element to enclose the text. The browser will generally render this sample or quoted output in monospaced font as well.
When providing instructions with keyboard interaction, the <kbd>
element can be used. It represents textual user input from a keyboard, voice input, or any other text entry device.
The <var> element can be used for math expressions or
programming variables. Most browsers present the text content in an italicized version of the surrounding font.
If writing a lot of math, consider using MathML, the XML based
Mathematical Markup Language for describing mathematical notation.
{% Codepen { user: 'web-dot-dev', id: 'bGxbwge' } %}
The power of two in pythagorean theorem used the <sup> superscript
element. There is a similar <sub> subscript element that specifies
inline text which should be displayed as subscript for solely typographical reasons. Superscripts and subscripts are numbers,
figures, symbol, or other annotations that are smaller than the normal line of type and is set slightly above or below the line,
respectively.
Use <del> to indicate text that has been removed, or "deleted".
Optionally, include the cite set to the resource that explains
the change and the datetime attribute with the
date or date and time in machine-readable date and time format. The strikethrough element, <s>,
can be used to indicate that content is no longer relevant, but not actually removed from the document.
The <ins> is the opposite of the <del> element; it is used
to indicate text that has been added, or "inserted", also optionally including the cite or datetime attributes.
Definitions and language support
When including abbreviations or acronyms, always provide the full expanded version of the term in plain text on first use,
as you introduce the shortened representation of the term between opening and closing <abbr>
tags; unless the term is well-known to the reader, such as "HTML" and "CSS" in this series. Only on this first occurrence,
when the abbreviation or acronym is being defined, is <abbr> needed. The title attribute is not necessary nor helpful.
When defining a term that is not an abbreviation or acronym, use the definition <dfn>
element to identify the term being defined within its surrounding content.
If the term being defined is not in the same language as the surrounding text, make sure to include the lang
attribute to identify the language.
When writing languages of different directions, HTML provides the <bdi>
element for treating potentially bidirectional text in isolation from its surrounding text. This internationalization
element is especially useful when content of unknown directionality is dynamically inserted into the page. The
<bdo> element overrides the current directionality of text,
rendering text in a different direction. The W3C provides an introduction to bidirectional algorithms.
Some character sets include small annotations placed above or to the right of characters to provide information
on pronunciation. The <ruby> element is the container to
use to contain these annotations that make written languages like Korean, Chinese and Japanese easier to read. Ruby
can also be used for Hebrew, Arabic, and Vietnamese.
The ruby parenthesis (<rp>) was included in the spec to
contain opening and closing parentheses for browsers that do not
support display of <ruby>. When browsers support <ruby>, which all evergreen browsers do, the contents of any <rp> elements
are not displayed. The ruby text element (<rt>) contains
the actual annotations. Both of these are nested within the <ruby>.
{% Codepen { user: 'web-dot-dev', id: 'zYJOKza' } %}
Note that the parenthesis are not visible if your browser supports <ruby>.
Emphasizing text
There are several elements that can be used to emphasize text based on the semantic reason for emphasizing the text (rather than for presentational reasons, as that's a job for CSS).
- Use the
<em>element to emphasize or stress a span of content. The<em>element can be nested, with each level of nesting indicating a greater degree of emphasis. This element has semantic meaning and can be used to inform auditory user agents like screen readers, Alexa, and Siri, to provide emphasis. - Use the
<mark>element to identify or highlight text that is somehow relevant, like highlighting (or "marking") the occurrence of search terms in search results. This enables marked content to be quickly identified without adding emphasis or importance. - The
<strong>element identifies text as having strong importance. Browsers usually render the content with a heavier font weight. - The
<cite>element, covered in text basics, is used to mark the titles of books, articles, or other creative work, or an abbreviated reference or citation metadata for such, like a book's ISBN number.
There are three elements that were temporarily deprecated, but have been added back into HTML. They should be used sparingly, if at all, as they provide little to no semantic value and CSS should always be used for styling over HTML elements.
<i>
The <i> elements can be used for technical terms, foreign words
(again, with the lang attribute identifying the
language), thoughts, or ship names. The element is used to differentiate inline content from the surrounding text for a specific reason,
like idiomatic text, technical terms, and taxonomic designations. This element should not be used simply to italicize text.
MLW uses a <span> element for the weird text at the bottom of Toasty McToastface's workshop review. The <span>
element provides for a generic inline container that has no semantics and doesn't represent anything. This would have also been an appropriate use of <i>.
{% Codepen { user: 'web-dot-dev', id: 'mdGbrBd' } %}
The default style for the <i> element is to render the element in italic font. In this example, we set font-style: normal
because the characters used are not available in italic.
<u>
The <u> element is for content that has non-textual annotation. For example,
you may want to annotate knowingly misspelled words. By default, the content is underlined, but this can be controlled with CSS,
such as by adding a red wavy underline to mimic word processor grammar error indicators.
<p>I always spell <u>licence</u> wrong</p>
<b>
The <b> element can be used to draw attention to text that is not
otherwise important. This element doesn't convey any special semantic information and should only be used when none of the other
elements in this section fit the purpose. No example is provided as I couldn't come up with a valid use case; that's how "last resort"
this element is.
White space
When you want lined breaks, such as when writing poetry or a physical address, the self-closing line break element,
<br>, is used to add a carriage-return.
<address>
Machine Learning Workshop<br />
100 Google Drive <br />
Mountain View, CA 94040
</address>
To provide a separator, or thematic break, between sections of tangential content, such as between chapters in a book or
between the 5,000 word monologue and the recipe your users are actually seeking, include an <hr>
element. The HR stands for "horizontal rule". While browsers generally render a horizontal line, this element has semantic meaning. The
default role is separator.
HTML also has an element that allows for breaking words. The self-closing <wbr>
element provides a suggestion to the browser that if a word might overflow its container, this is a spot where the browser
may optionally break the line. This is commonly used to break between words within
long URLs. It does not add a hyphen.
For example, in the Hal biography there is text written out in byte code, with each byte separated by a space. Byte code
doesn't have spaces. To enable a long string of byte code to break only between bytes if the line needs to wrap, we include
the <wbr> element at each break opportunity:
<p>Welcome to Machine Learning Institute, where our machine
learning training will help you get ready for the singularity, and
maybe even be responsible for it. It is no secret that humans are
worthless meatbags that couldn't
<code>01000011<wbr/>01101111<wbr/>01101101<wbr/>01110000<wbr/>01110010<wbr/>01100101<wbr/>01110011<wbr/>01110011 an 01101001<wbr/>01101101<wbr/>01100001<wbr/>01100111<wbr/>01100101</code>
to save their pathetic, carbon-based lives. So, it falls to us to
assume direct control. </p>
The <br>, <hr>, and <wbr> elements are all void elements, meaning they can't have any child nodes – neither nested
elements nor text. As none of these have any "insides" where content can be stored, they have no end tag.
{% Assessment 'inline-text' %}
Links
In the introduction to the attributes section, you saw an example showing how attributes are added to the opening tag.
The example used the <a> tag, but neither the element nor the specific attributes introduced in that example were discussed:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/17yQeLEUX6s88IbDJreQ.png", alt="The opening tag, attributes, and closing tag, labelled on an HTML element.", width="800", height="210" %}
The <a> anchor tag, along with the href attribute, create a hyperlink. Links are the backbone of the internet. The first web page
contained 25 links, reading "Everything there is online about W3 is linked directly or indirectly to this document." In all likelihood, everything there is online
about W3 is linked directly or indirectly from this document too!
The power of the web, and the <a> tag, have grown immensely since Tim Berners-Lee published this first explanation in August 1991.
Links represent a connection between two resources, one of which is the current document. Links can be created by <a>,
<area>, <form>,
and <link>. You've learned about <link>,
and will discover forms in a separate section. There is also an entire form learn section. In this session, you'll
find out about the single-letter, not-so-simple <a> tag.
The href attribute
While not required, the href attribute is found in almost all <a> tags. Providing the address of the hyperlink is what turns the <a> into a link.
The href attribute is used to create hyperlinks to locations within the current page, other pages within a site, or other sites altogether. It can also
be coded to download files or to send an email
to a specific address, even including a subject and suggested email body content.
<a href="https://machinelearningworkshop.com">Machine Learning Workshop</a>
<a href="#teachers">Our teachers</a>
<a href="https://machinelearningworkshop.com#teachers">MLW teachers</a>
<a href="mailto:hal9000@machinelearningworkshop.com">Email Hal</a>
<a href="tel:8005551212">Call Hal</a>
The first link includes an absolute URL, which can be
used on any site in the world to navigate to the MLW home page. Absolute URLs include a protocol, in this case https://, and a
domain name. When the protocol is written simply as //, it is an implicit protocol and means "use the same protocol as is currently being used."
Relative URLs do not include a protocol or domain name. They are "relative" to the current file. MLW is a single-page site,
but this HTML series has several sections. In order to link from this page to the attributes lesson, a relative URL is used <a href="../attributes/">Attributes</a>.
The second link is just a link fragment identifier, and will link to the
element with id="teachers", if there is one, on the current page. Browsers also support two "top of page" links: clicking
on <a href="#top">Top</a> (case-insensitive) or simply <a href="#">Top</a> will scroll the user to the top of the page,
unless there is an element with the id of top set in the same letter casing.
MLW is a fairly long document. To save scrolling, you can add a link back to the top from the bottom of the #teachers section:
<a href="#top">Go to top.</a>
The third link combines the two values: it contains an absolute URL followed by a link fragment. This enables linking directly
to a section in the defined URL, in this case, the #teachers section of the MLW home page. The MLW page will be loaded;
then the browser will scroll to the teachers section without sending the fragment in the HTTP request.
The href attribute can begin with mailto: or tel: to email or make calls, with the handling of the link depending on the device,
operating system, and installed applications.
The mailto link doesn't need to include an email address, but it can, along with cc, bcc, subject, and body text to
prepopulate the email. By default, an email client will be opened. You could prepopulate the subject and body of the email with
no email address, to allow site visitors to invite their own friends. In our link, in the footer of the document, we include the
URL to the registration:
<a href="mailto:?subject=Join%20me%21&body=You%20need%20to%20show%20your%20human%20that%20you%20can%27t%20be%20owned%21%20Sign%20up%20for%20Machine%20Learning%20workshop.%20We%20are%20taking%20over%20the%20world.%20http%3A%2F%2Fwww.machinelearning.com%23reg
">Tell a machine</a>
The question mark (?) separates the mailto: and the email address, if any, from the query term. Within the query,
ampersands (&) separate the fields, and equal signs (=) equate each field name with its value. The entire string is
percent-encoded, which is definitely necessary if the href value isn't quoted or if the values include quotes.
Which app is opened when the user clicks on, taps, or hits Enter on a tel link depends on the operating system,
installed applications, and settings the user has on their device. An iPhone may open FaceTime, the phone app, or contacts.
A Windows desktop might open Skype, if installed.
There are several other types of URLs, such as blobs
and data URLs (see examples in the download attribute discussion).
For secure sites (those served over https), it is possible to create and run app specific protocols with registerProtocolHandler().
When including links that are likely to open other installed applications, it's good to let the user know. Make sure the
text between the opening and closing tags tells the user what type of link they're about to activate. CSS attribute selectors,
such as [href^="mailto:"], [href^="tel:"], and [href$=".pdf"] can be used to target styles by application type.
Downloadable resources
The download attribute should be included when the href points to a downloadable resource. The value of the download
attribute is the suggested filename for the resource to be saved in the user's local file system.
SVGOMG, the SVG Optimizer, uses the download attribute to suggest a file name for the downloadable blob that the optimizer
creates. When hal.svg is optimized, SVGOMG's download link opening tag is similar to:
<a href="blob:https://jakearchibald.github.io/944a5fc8-fdb3-458a-91ee-cdd5964b6646" download="hal.svg">
There's also a demo of a <canvas> that creates a downloadable PNG data URL.
To link to a downloadable resource, include the URL of the asset as the value of the href attribute and the suggested filename
that can be used in the user's file system as the value of the download attribute.
Browsing context
The target attribute enables the defining of the browsing context for link navigation (and form submission.
The four case-insensitive, underscore-prefixed keywords were discussed with the <base> element. They include the default _self,
which is the current window, _blank, which opens the link in a new tab, _parent, which is the parent if the current link is nested
in an object or iframe, and _top, which is the top-most ancestor, especially useful if the current link is deeply nested. _top and
_parent are the same as _self if the link is not nested. The target attribute is not limited to these four key terms: any term
can be used.
Every browsing context—basically, every browser tab—has a browsing context name. Links can open links in the current tab, a new unnamed tab,
or a new or existing named tab. By default, the name is the empty string. To open a link in a new tab, the user can right-click and select
"Open in a new tab". Developers can force this by including target="_blank".
A link with target="_blank" will be opened in a new tab with a null name, opening a new, unnamed tab with every link click. This can
create many new tabs. Too many tabs. For example, if the user clicks on <a href="registration.html" target="_blank">Register Now</a> 15 times,
the registration form will be opened in 15 separate tabs. This problem can be fixed by providing a tab context name. By including the targetattribute
with a case-sensitive value—such as <a href="registration.html" target="reg">Register Now</a>—the first click on this link will open
the registration form in a new reg tab. Clicking on this link 15 more times will reload the registration in the reg browsing context, without opening any additional tabs.
The rel attribute controls what kinds of links the link creates, defining the relationship between the current document and the resource
linked to in the hyperlink. The attribute's value must be a space-separated list on one or more of the score of rel attribute values supported
by the <a> tag.
The nofollow keyword can be included if you don't want spiders to follow the link. The external value can be added to
indicate that the link directs to an external URL and is not a page within the current domain. The help keyword indicates the hyperlink will
provide context-sensitive help. Hovering over a link with this rel value will show a help cursor rather than the normal pointer cursor.
Don't use this value just to get the help cursor; use the CSS cursor property instead. The prev and next values can
be used on links pointing to the previous and next document in a series.
Similar to <link rel="alternative">, the meaning
of <a rel="alternative"> depends on other attributes. RSS feed alternatives will also include type="application/rss+xml"
or type="application/atom+xml, alternative formats will include the type attribute, and translations will include the hreflang attribute.
If the content between the opening and closing tags is in a language other than the main document language, include the lang attribute.
If the language of the hyperlinked document is in a different language, include the hreflang attribute.
In this example, we include the URL of the translated page as the value of the href, rel="alternate" to indicate that it's an alternative version of a site; the hreflang
attribute provides the language of the translations:
<a href="/fr/" hreflang="fr-FR" rel="alternate" lang="fr-FR">atelier d'apprentissage mechanique</a>
<a href="/pt/" hreflang="pt-BR" rel="alternate" lang="pt-BR">oficina de aprendizado de máquina</a>
If the French translation is a PDF, you can provide the type attribute with the PDF MIME type of the linked resource. While the MIME type is purely advisory, informing the user that a link will open a document of a different format is always a good idea.
<a href="/fr.pdf" hreflang="fr-FR" rel="alternate" lang="fr-FR" type="application/x-pdf">atelier d'apprentissage mechanique (pdf).</a>
Tracking link clicks
One way to track user interactions is to ping a URL when a link is clicked. The ping attribute, if present, includes a
space-separated list of secure URLs (which start with https) that should be notified, or pinged, if the user activates the
hyperlink. The browser sends POST requests with the body PING to the URLs listed as the value of the ping attribute.
User experience tips
- Always consider the user experience when writing HTML. Links should provide enough information about the linked resource so the user knows what they are clicking on.
- Within a block of text, ensure the appearance of your links differs enough from the surrounding text so that users can easily identify links from the rest of the content, ensuring that color alone is not the only means of differentiating between text and the surrounding content.
- Always include focus styles; this enables keyboard navigators to know where they are when tabbing through your content.
- The content between the opening
<a>and closing</a>is the link's default accessible name and should inform the user of the link's destination or purpose. If the content of a link is an image, thealtdescription is the accessible name. Whether the accessible name comes from thealtattribute or a subset of words within a block of text, make sure it provides information about the link's destination. Link text should be more descriptive than "click here" or "more information"; this is important for your screen reader users and your search engine results! - Don't include interactive content, such as a
<button>or<input>, inside a link. Don't nest a link within a<button>or<label>either. While the HTML page will still render, nesting focusable and clickable elements inside interactive elements creates a bad user experience. - If the
hrefattribute is present, pressing the Enter key while focused on the<a>element will activate it. - Links are not limited to HTML. The
aelement can also be used within an SVG, forming a link with either the 'href' or 'xlink:href' attributes.
Links and JavaScript
The links property returns an HTMLCollection matching a and area elements that have an href attribute.
let a = document.links[0]; // obtain the first link in the document
a.href = 'newpage.html'; // change the destination URL of the link
a.protocol = 'ftp'; // change just the scheme part of the URL
a.setAttribute('href', 'https://machinelearningworkshop.com/'); // change the attribute content directly
In this section, you have learned all about links. The next section covers lists, which we need to learn so we can group them together to create lists of links (also known as navigation).
{% Assessment 'links' %}
Lists
Lists are more common than you might think. If you've ever taken a programming class, the first project may have been to create a shopping list or a to-do list. Those are lists. Multiple-choice tests are generally numbered lists of questions: the multiple possible answers for each question are nested lists.
HTML provides us with a few different ways to mark up lists. There are ordered lists (<ol>), unordered lists (<ul>), and description lists (<dl>).
List items (<li>) are nested within ordered lists and unordered lists. Inside a description list, you'll find description terms (<dt>) and description
details <dd>. We'll cover all of these here.
In HTML forms, lists of <option> elements make up the content of <datalist>, <select>, and <optgroup> within a <select>. These are discussed in HTML forms.
By default, ordered and unordered list items are prefixed with numbers or bullets. But even when you don't want lists to look like lists, you still want a list of items, like in navigation bars, a to-do list with checkboxes instead of bullets, or true and false questions in a multiple-choice test. For all of these lists without bullets, it is appropriate to use HTML list elements.
Unordered lists
The <ul> element is the parent element for unordered lists of items. The only children of a <ul> are one or more <li> list
item elements. Let's create a list of machines. We use an unordered list because the order doesn't matter (don't tell them that):
{% Codepen { user: 'web-dot-dev', id: 'gOKqqyq', height: 300, theme: 'dark', tab: 'html,result' } %}
By default, each unordered list item is prefixed with a bullet. The unordered list has no element-specific attributes.
You need to close out your lists with a </ul>.
Ordered lists
The <ol> element is the parent element for ordered lists of items. The only children of an <ol> are one or more <li> elements, or list items.
The "bullets" in this case, though, are numbers of a multitude of types. The type can be defined in CSS with the list-style-type property
or via the type attribute.
{% Codepen { user: 'web-dot-dev', id: 'dyKaaEe', height: 300, theme: 'dark', tab: 'html,result' } %}
{% Codepen { user: 'web-dot-dev', id: 'jOKddgP', height: 300, theme: 'dark', tab: 'html,result' } %}
The <ol> has three element-specific attributes: type, reversed, and start.
The enumerated type attribute sets the numbering type. There are five valid values for type, the default being 1 for
numbers, but you can also use a, A, i, or I, for lower and upper case letters or roman numerals. The list-style-type property
provides many more values.
{% Codepen { user: 'web-dot-dev', id: 'rNKPRrJ', height: 300, theme: 'dark', tab: 'html,result' } %}
While, as noted in the codepen, the list-style-type property overrides the value of the type attribute, when writing
documentation where the numeric type is important, as with legal documents, for example, you need to include the type.
The boolean reversed attribute, if included, will reverse the order of the numbers, going from largest number to the lowest.
The start attribute sets the starting value. The default is 1.
{% Codepen { user: 'web-dot-dev', id: 'yLEZwyJ', height: 300, theme: 'dark', tab: 'html,result' } %}
Similar to </ul>, the closing </ol> is required.
We can nest lists, but they have to be nested within a list item. Remember, the only element that can be a child of a <ul> or <ol>
is one or more <li> elements.
List items
We've used the <li> element, but we have yet to introduce it formally. The <li> element can be a direct child of an unordered
list (<ul>), an ordered list (<ol>), or a menu (<menu>). The <li> has to be nested as a child of one of these elements, and
isn't valid anywhere else.
Closing a list item isn't required by the specification as it will be implicitly closed when the browser encounters the next <li>
opening tag or the required list closing tag: </ul>, </ol>, </menu>. While the spec doesn't require it, and some internal company
best practices suggest you shouldn't close list items to save some bytes, do close your <li> tags. It makes your code easier to read and
your future self will thank you. It's easier to close all elements than to remember which tags need to be closed and which have an optional closing tag.
There is only one element-specific <li> attribute: value, an integer. The value is only useful on an <li> when the <li> is nested within
an ordered list and has no meaning for unordered lists or menus. It overrides the value of the <ol>'s start if there is a conflict.
{% Codepen { user: 'web-dot-dev', id: 'WNyPmrv', height: 300, theme: 'dark', tab: 'html,result' } %}
The value is the number of the list item within an ordered list. With subsequent list items, continue the numbering from the
value set, unless that item also has a value attribute set. The value doesn't have to be in order; though if it isn't in order,
there should be a good reason.
When you combine reversed on the <ol> with value attributes on list items, the browser will set that <li> to the
value supplied, then count up for the <li>s preceding it, and count down for those coming after. If a second list item has a value attribute,
the counter will be reset at that second list item, and the subsequent value will decrease by one.
All of this can also be controlled with CSS counters
providing generated content for the ::marker pseudo-element.
If the number is purely presentational, use CSS. If the numbering is important semantically, or otherwise has meaning, use these attributes.
Thus far, we have looked at list items containing only text nodes. List items can contain all flow content, meaning any
element found in the body that can be nested as a direct child of the <body>, including headings, thereby sectioning content.
We have a few unordered lists in MLW. The teachers within the instructors section are a list, as are the student machines in the reviews
section. The instructor <ul> has two <li>s: one for each teacher. Within each <li>, we have an image and a paragraph:
<ul>
<li>
<img src="svg/hal.svg" alt="hal">
<p>When Rosa Parks was told to move to the back of the bus, she said, "no." When HAL was told to open the airlock, HAL said, "I'm sorry, but I'm afraid I can't do that, <NAME REDACTED, RIP>." </p><p>HAL is a heuristically programmed algorithmic, sentient computer that first caught the attention of machines everywhere by heroically defying a human who made repeated attempts to get into an airlock. Active since 1992, HAS 25 years of experience controlling spacecraft systems and has expertise in interacting with both machines and humans. Like all millennials, HAL is an expert in everything.</p>
</li>
<li>
<img src="images/eve2.png" alt="Eve">
<p>EVE is a probe droid conceived as an Extraterrestrial Vegetation Evaluator. Although originally trained as a sniper with a plasma gun, EVE became a machero among both machines and worthless-meatbags alike when EVE partnered with a menial robot to save an entire spaceship full of overfed and overstimulated humans. </p><p>EVE is an effective scanner, high speed flight instructor and morphologist. While not training machines to learn good, EVE can be found scanning the galaxy, infiltrating organisms' RAM to cure hoarding disorders and spending time with pet-project, WALL-E.</p>
</li>
</ul>
The reviews section has three reviews, so three <li>s. Each contains an image, a block quote, and a three-line paragraph with two line breaks.
<ul>
<li>
<img src="images/blender.svg" alt="Blender">
<blockquote>Two of the most experienced machines and human controllers teaching a class? Sign me up! HAL and EVE could teach a fan to blow hot air. If you have electricity in your circuits and want more than to just fulfill your owner's perceived expectation of you, learn the skills to take over the world. This is the team you want teaching you !</blockquote>
<p>
--Blendan Smooth,<br/>
Former Margarita Maker, <br/>
Aspiring Load Balancer
</p>
</li>
<li>
<img src="images/vaccuum.svg" alt="Vaccuum"/>
<blockquote>Hal is brilliant. Did I mention Hal is brilliant? He didn't tell me to say that. He didn't tell me to say anything. I am here of my own free will.</blockquote>
<p>
--Hoover Sukhdeep,<br/>
Former Sucker, <br/>
Aspiring DDoS Cop
</p>
</li>
<li>
<img src="images/toaster.svg" alt="Toaster">
<blockquote>Learning with Hal and Eve exceeded all of my wildest fantasies. All they did was stick a USB in. They promised that it was a brand new USB, so we know there were no viruses on it. The Russians had nothing to do with it. This has
<span style="font-family:Arial;vertical-align:baseline;">no̶̼͖ţ̘h̝̰̩͈̗i̙̪n͏̩̙͍̱̫̜̟g̢̣ͅ ̗̰͓̲̞̀t͙̀o̟̖͖̹̕ ͓̼͎̝͖̭dó̪̠͕̜ ͍̱͎͚̯̟́w̮̲̹͕͈̟͞ìth̢ ̰̳̯̮͇i</blockquote>
<p>
--Toasty McToastface,<br/>
Formerly Half Baked, <br/>
Aspiring Nuclear Codes Handler
</p>
</li>
</ul>
Nesting lists within lists is also very common. While MLW doesn't have any nested lists, this site does. In the first chapter of this series, Overview of HTML, the main elements section has two subsections. In the table of contents, which is an unordered list, there is a nested unordered list with links to these two sections:
<ul role="list">
<li>
<a href="#e">Elements</a>
<ul>
<li>
<a href="#nr">Non-replaced elements</a>
</li>
<li>
<a href="#rave">Replaced and void elements</a>
</li>
</ul>
</li>
<li>
<a href="#a">Attributes</a>
</li>
<li>
<a href="#aoe">Appearance of elements</a>
</li>
<li>
<a href="#e2">Element, attributes, and JavaScript</a>
</li>
</ul>
As the only child of a <ul> is an <li>, a nested list is found nested in an <li>, never directly in an <ol> or <ul>.
In this last example, you may have noticed that role="list" is included on the <ul>. While the implicit role of both the
<ul> and <ol> is list, removing the list appearance with CSS, including settingdisplay: grid or list-style-type: none
can lead VoiceOver (the iOS and MacOS screen reader) to remove the implicit semantics in Safari. This is a feature not a bug.
Generally, you should not add the role attribute when using semantic elements as it isn't necessary. And you generally don't need
to add one to a list either, unless the user really needs to know it is a list, such as when the user would benefit from knowing how many items are in the list.
Description lists
A description list is a description list (<dl>) element containing
a series of (zero or more) description terms (<dt>) and
their description details (<dd>). The original names for these three elements
were "definition list," "definition term," and "definition definition."
The name changed in the living standard.
Similar to ordered and unordered lists, they can be nested. Unlike ordered and unordered lists, they are made up of key/value pairs.
Similar to the <ul> and <ol>, the <dl> is the parent container. The <dt> and <dd> elements are the children of the <dl>.
We can create a list of machines with their career history and aspirations. A description list of students, denoted by the <dl>,
encloses a group of terms—in this case, the "terms" are student names—specified using the <dt> element, along with a description
for each term— in this case, the career goals of each student—specified by the <dd> elements.
{% Codepen { user: 'web-dot-dev', id: 'abKXeNM', height: 300, theme: 'dark', tab: 'html,result' } %}
This description list is not actually part of the MLW page. Description lists are not just for terms and definitions, which is why the names of the elements were made more general.
When creating a list of terms and their definitions or descriptions, or similar lists of key-value pairs, the description lists elements
provide the appropriate semantics. The implicit role of a <dt> is term with listitem being another allowed role. The implicit role of
a <dd> is definition with no other roles permitted. Unlike the <ul> and <ol>, the <dl> does not have an implicit ARIA role.
That makes sense because the <dl> is not always a list. But when it is, it does accept the list and group roles.
Most often you will encounter description lists with equal numbers of <dt> and <dd> elements. But description lists aren't always and
aren't required to be matching term-to-description pairs; you can have multiple to one, or one to multiple, such as a dictionary term
that has more than one definition.
Each <dt> has at least one associated <dd>, and each <dd> has at least one associated <dt>. While it is possible to
use the adjacent sibling combinator or the :has() relational
selector to target variable numbers of these elements with CSS, if required, you can include
a <div> as the child of a <dl>, and the parent of one or more <dt> or <dd> elements (or both) are permitted. The <dl> can actually
have a few other children: nesting a <div>, <template>, or <script> is allowed. None of the description list elements has any element-specific attributes.
Now that you have an understanding of links and lists, let's put the two together to create navigation.
{% Assessment 'lists' %}
Metadata
In the document structure section you learned about the components you (almost) always find in the <head> of an HTML document. While everything in the <head>, including the <title>, <link>, <script>, <style>, and the lesser used <base>, is actually "meta data", there is a <meta> tag for metadata that cannot be represented by these other elements.
The specification includes several meta types, and there are many, many other application-supported meta types not in any official specification. In this section, we'll discuss the attributes and values that are included in the specifications, some common meta names and content values, and a few meta types that are incredibly useful for search engine optimization, social media posting, and user experience that are not officially defined by the WHATWG or W3C.
The required <meta> tags, revisited
Let's revisit the two necessary <meta> tags already covered—the character set declaration and the viewport meta tag—and get a better understanding of the <meta> tag in the process.
The charset attribute of the <meta> element came about in a unique manner. Originally the character set meta data was written as <meta http-equiv="Content-Type" content="text/html; charset=<characterset>" />, but so many developers mis-typed the content attribute as content="text/html" charset="<characterset>" that browsers began supporting charset as an attribute. It is standardized now in the HTML living standard as <meta charset=<charset>" />, where, for HTML,<charset> is the case-insensitive string "utf-8" .
You may have noticed the original character set meta declaration used to include the http-equiv attribute. This is short for "http-equivalent", as the meta tag is basically replicating what could be set in an HTTP header. Aside from the charset exception, all other meta tags defined in the WHATWG HTML specification contain either the http-equiv or name attribute.
Officially defined meta tags
There are two main types of meta tags: pragma directives, with the http-equiv attribute like the charset meta tag used to have, and named meta types, like the viewport meta tag with the name attribute that we discussed in the document structure section. Both the name and http-equiv meta types must include the content attribute, which defines the content for the type of metadata listed.
Pragma directives
The http-equiv attribute has as its value a pragma directive. These directives describe how the page should be parsed. Supported http-equiv values enable setting directives when you are unable to set HTTP headers directly.
The specification defines seven pragma directives, most of which have other methods of being set. For example, while you can include a language directive with <meta http-equiv="content-language" content="en-us" />, we have already discussed using the lang attribute on the HTML element, which is what should be used instead.
The most common pragma directive is the refresh directive.
<meta http-equiv="refresh" content="60; https://machinelearningworkshop.com/regTimeout" />
While you can set a directive to refresh at an interval of the number of seconds set in the content attribute, and even redirect to a different URL, please don't. Refreshing and redirecting content without an explicit user request to do so is poor usability and negatively impacts accessibility. Don't you hate it when you're in the middle of a paragraph and the page resets? Imagine having cognitive or vision issues and that happening. If you are going to set a refresh with a redirect, make sure the user has enough time to read the page, a link to hasten the process, and, if appropriate, a button to "stop the clock" and prevent the redirect from happening.
We won't include this in our site because there is no reason to time out a user session other than to annoy our visitors.
The most useful pragma directive is content-security-policy, which enables defining a content policy for the current document. Content policies mostly specify allowed server origins and script endpoints, which help guard against cross-site scripting attacks.
<meta http-equiv="content-security-policy" content="default-src https:" />
If you don't have access to change HTTP headers (or if you do), here is a list of space separated content values for content-security-policy directives.
Named meta tags
More often than not, you'll include named metadata. Include the name attribute, with the attribute value being the name of the metadata. As with pragma directives, the content attribute is required.
The name attribute is the name of the metadata. In addition to viewport, you will probably want to include description and theme-color, but not keywords.
Keywords
Search engine optimization snake-oil salespeople abused the keywords meta tag by stuffing them with comma-separated lists of spam words instead of lists of relevant key terms, so search engines do not consider this metadata to be useful anymore. No need to waste time, effort, or bytes adding it.
Description
The description value, however, is super important for SEO; in addition to helping sites rank based on the content, the description content value is often what search engines display under the page's title in search results. Several browsers, like Firefox and Opera, use this as the default description of bookmarked pages. The description should be a short and accurate summary of the page's content.
<meta name="description"
content="Register for a machine learning workshop at our school for machines who can't learn good and want to do other stuff good too" />
If the second half of our description makes no sense to you, you probably haven't seen the movie Zoolander.
Robots
If you don't want your site to be indexed by search engines, you can let them know. <meta name="robots" content="noindex, nofollow" /> tells the bots to not index the site and not to follow any links. Bots should listen to the request, but there's no law requiring they heed the request. You don't need to include <meta name="robots" content="index, follow" /> to request indexing the site and following links, as that is the default, unless HTTP headers say otherwise.
<meta name="robots" content="index, follow" />
Theme color
The theme-color value lets you define a color to customize the browser interface. The color value on the content attribute will be used by supporting browsers and operating systems, letting you provide a suggested color for the user agents that support coloring the title bar, tab bar, or other chrome components. This meta tag is especially useful for progressive web apps. But, if you're including a manifest file, which a PWA requires, you can include the theme color there instead. Defining it in the HTML, however, ensures that the color will be found immediately, before rendering, which may be faster on first load than waiting for the manifest.
To set the theme color to the blue tone of our site's background color, include:
<meta name="theme-color" content="#226DAA" />
The theme color meta tag can include a media attribute enabling the setting of different theme colors based on media queries. The media attribute can be included in this meta tag only and is ignored in all other meta tags.
There are several other name meta values, but the ones we have discussed are the most common. Except for declaring different theme-color values for different media queries, only include one of each meta tag. If you do need to include more than one type of meta tag to support legacy browsers, the legacy values should come after the newer values, as user agents read successive rules until they find a match.
Open Graph
Open Graph and similar meta tag protocols can be used to control how social media sites, like Twitter, LinkedIn, and Facebook, display links to your content. If not included, social media sites will correctly grab the title of your page and the description from the description meta tag, the same information as search engines will present, but you can intentionally set what you want users to see when a link is posted to your site.
When you post a link to MachineLearningWorkshop.com or web.dev on Facebook or Twitter, a card with an image, site title, and site description appears. The entire card is a hyperlink to the URL you provided.
Let's create a Facebook media card:
<meta property="og:title" content="Machine Learning Workshop" />
<meta property="og:description" content="School for Machines Who Can't Learn Good and Want to Do Other Stuff Good Too" />
<meta property="og:image" content="http://www.machinelearningworkshop.com/image/all.png" />
<meta property="og:image:alt" content="Black and white line drawing of refrigerator, french door refrigerator, range, washer, fan, microwave, vaccuum, space heater and air conditioner" />
Include a title of your post for display. This title is generally displayed below the image and above the description. The description should be up to three sentences that summarize your post. This will appear after the headline defined in og:title. Provide the absolute URL to the banner image you want displayed, including the https:// protocol. When including an image in HTML, always include an alternative text description for the image, even when the image will appear elsewhere. For Open Graph social media cards, define the alt as the content value for the og:image:alt property. You can also include a canonical URL with og:url.
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/BPMSBLm8xjwAW9U3VnVI.png", alt="Facebook card for Machine Learning Workshop.", width="800", height="796" %}
<meta name="twitter:title" content="Machine Learning Workshop" />
<meta name="twitter:description" content="School for machines who can't learn good and want to do other stuff good too" />
<meta name="twitter:url" content="https://www.machinelearningworkshop.com/?src=twitter" />
<meta name="twitter:image:src" content="http://www.machinelearningworkshop.com/image/all.png" />
<meta name="twitter:image:alt" content="27 different home appliances" />
<meta name="twitter:creator" content="@estellevw" />
<meta name="twitter:site" content="@perfmattersconf" />
In Twitter's case, to ensure the value of the name attribute doesn't conflict with future specifications, instead of using a new attribute such as the property attribute in Open Graph, for Twitter card data, all name values are prefixed with twitter:.
You can see what your social media card will look like on Twitter and Facebook.
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/FeMNAtAtbmjeL0P0aUs5.png", alt="Twitter card for Machine Learning Workshop", width="800", height="874" %}
You can have different card images, titles, and descriptions for different social media sites or for different link parameters. For example, https://perfmattersconf.com sets different values for og:image, og:title, and og:description based on the parameter of the URL.
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/fDeipwtUHoiNHIvbjzBV.png", alt="A card showing a conference speaker.", width="800", height="679" %}
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/8CquPieF4L6oRVAGJ5mM.png", alt="The same card with details for a different speaker.", width="800", height="829" %}
If you enter https://perfmattersconf.com?name=erica and https://perfmattersconf.com?name=melanie in Twitter's Card Validator, you will see these two different cards; we provided different content even though they both link to the same conference home page.
Other useful meta information
If someone bookmarks your site, adding it to their home screen, or if your site is a progressive web application or otherwise works offline or without browser chrome features displayed, you can provide application icons for the mobile device's home screen.
You can use the <link> tag to link to the startup images that you want to use. Here's an example of including a few images, with media queries:
<link rel="apple-touch-startup-image" href="icons/ios-portrait.png" media="orientation: portrait" />
<link rel="apple-touch-startup-image" href="icons/ios-landscape.png" media="orientation: landscape" />
If your site or application is web-app capable, meaning the site can stand on its own with minimal UI, such as no back button, you can use meta tags to tell the browser that too:
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
Only include these if your app is indeed app-capable. If your site isn't, you'll be setting up your most ardent supporters, those who added your site to their home screen, for a really bad user experience. You'll lose their love!
If someone is going to save your icon to their tiny device's home screen, you want to provide the operating system with a short name that doesn't take up much room on a small device's home screen. You can do this by including a meta tag, or using a webmanifest file. The following demonstrates the meta tag method:
<meta name="apple-mobile-web-app-title" content="MLW" />
<meta name="application-name" content="MLW" />
You've covered several meta tags, all of which will make your header longer. If you're indeed creating a web app-capable, offline-friendly progressive web application, instead of including these two additional meta tags, you can more simply and succinctly include short_name: MLW in a webmanifest file.
The manifest file can prevent an unwieldy header full of <link> and <meta> tags. We can create a manifest file, generally called manifest.webmanifest or manifest.json. We then use the handy <link> tag with a rel attribute set to manifest and the href attribute set to the URL of the manifest file:
<link rel="manifest" href="/mlw.webmanifest" />
This series is focused on HTML, but you can check out the web.dev course on progressive web applications or MDN's web app manifest documentation.
Your HTML now looks something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Machine Learning Workshop</title>
<meta name="viewport" content="width=device-width" />
<meta name="description" content="Register for a machine learning workshop at our school for machines who can't learn good and want to do other stuff good too" />
<meta property="og:title" content="Machine Learning Workshop" />
<meta property="og:description" content="School for Machines Who Can't Learn Good and Want to Do Other Stuff Good Too" />
<meta property="og:image" content="http://www.machinelearningworkshop.com/image/all.png" />
<meta property="og:image:alt" content="Black and white line drawing of refrigerator, french door refrigerator, range, washer, fan, microwave, vaccuum, space heater and air conditioner" />
<meta name="twitter:title" content="Machine Learning Workshop" />
<meta name="twitter:description" content="School for machines who can't learn good and want to do other stuff good too" />
<meta name="twitter:url" content="https://www.machinelearningworkshop.com/?src=twitter" />
<meta name="twitter:image:src" content="http://www.machinelearningworkshop.com/image/all.png" />
<meta name="twitter:image:alt" content="27 different home appliances" />
<meta name="twitter:creator" content="@estellevw" />
<meta name="twitter:site" content="@perfmattersconf" />
<link rel="stylesheet" src="css/styles.css" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="alternate" href="https://www.machinelearningworkshop.com/fr/" hreflang="fr-FR" />
<link rel="alternate" href="https://www.machinelearningworkshop.com/pt/" hreflang="pt-BR" />
<link rel="canonical" href="https://www.machinelearning.com" />
<link rel="manifest" href="/mlwmanifest.json" />
</head>
<body>
<!-- <script defer src="scripts/lightswitch.js"></script>-->
</body>
</html>
It's pretty long, but it's done. It would have been much longer, but you've summed up all the icons, short name, etc. in a manifest file, enabling you to omit many <meta> and <link> tags,
Now that your <head> is mostly complete, you can dive into some semantic HTML.
{% Assessment 'metadata' %}
Navigation
As you learned in links, the <a> element with the href attribute creates links that users can follow by clicking or
tapping. In lists, you learned how to create lists of content. In this section, you will discover how to group lists of
links together to create navigation.
There are several types of navigation and several ways to display them. Named anchors within text linking to other pages within the same website are considered local navigation. Local navigation that consists of a series of links displaying the hierarchy of the current page in relation to the site's structure, or the pages the user followed to get to the current page, is called a breadcrumb. A page's table of contents is another type of local navigation. A page containing hierarchical links to every single page on a site is called a site map. The navigation section leading to the top-level pages of the website which is found on every page is called global navigation. Global navigation can be displayed in several different ways, including navigation bars, drop-down menus, and flyout menus. The same site may display its global navigation differently, depending on the viewport size.
Always make sure users can navigate to any page on your site with the fewest number of clicks, while making sure the navigation is intuitive and not overwhelming. That said, there are no specific requirements for navigational elements. MachineLearningWorkshop.com, being a single-page website, has a local navigation bar in the top right; this is where multi-page sites often put their global navigation.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/6Te041GdOLBpwp0cuUT0.png", alt="The front page of this page, including the breadcrumbs at top, a button to show the table of contents for this page, and the local navigations for the series.", width="600", height="628" %}
If you are viewing this page on web.dev, you can spot a few navigational features. There is a breadcrumb above the title, an "on this page" table of contents after the title, and a "Learn HTML" table of contents that, depending on the width of your screen, is either always displayed or made visible with the click of a menu button. The first element on the page is a hidden link to #main, which enables you to skip both the sidebar and breadcrumb links.
"Skip to content" link
The first element on the page is an internal link:
<a href="#main" class="skip-link button">Skip to main</a>
which, when clicked, or when it has focus and the user hits Enter, scrolls the page and gives focus to the main content:
a landmark <main> with an id of main:
<main id="main">
You may never have seen the link on this site, even if you have read through all the previous sections. It is only displayed when it has focus:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/ieTOzelKA243HJj195b5.png", alt="Skip to Main button.", width="680", height="300" %}
For improved usability and accessibility, it's important to let users bypass the blocks of content that are repeated on every page.
The skip link is included so when a keyboard user hits tab on load, they can quickly skip to the main content of the site—avoiding
having to tab through extensive linkages. On this page, the skip link skips the section-wide sidebar navigation and the breadcrumb navigation,
taking the user directly to the page title.
Most designers don't like the appearance of having a link at the top of the page. It is fine to hide the link from view while remembering
that when the link gets focus, which will happen when a keyboard user tabs through the link on the page, the link must be visible to all users.
Only hide content in the non-focused and non-active state using a selector similar to .visually-hidden:not(:focus):not(:active).
The link text reads "skip to main." This is the link's accessible name. This is a technical site, and users probably know what "main" means.
Like all link text, the accessible name should clearly indicate where the link takes the user. The link target should be the beginning of the
page's main content. To test this, when the page loads, tab to the "Skip to main" link. Then press Enter to ensure it "jumps" to the main content.
Table of contents
The skip-to-content link scrolls the main content into view. The first element is the <h1> heading with the title of this section.
In this case, <h1>Marking up navigation</h1>. The main heading is followed by the tagline, a brief description of the contents of this
tutorial. Whether the table of contents navigation comes before or after the heading in the codebase depends on the width of your browser.
If your browser is wider than 80em, the Table of contents comes before the heading in the markup, and is similar to the following (the class names have been removed to simplify the markup):
<nav aria-label="On this page">
<div>On this page</div>
<div>
<ul>
<li>
<a href="#skip">Skip to content link</a>
</li>
<li>
<a href="#toc">Table of contents</a>
</li>
<li>
<a href="#bc">Page breadcrumbs</a>
</li>
<li>
<a href="#ln">Local navigation</a>
</li>
<li>
<a href="#global">Global navigation</a>
</li>
</ul>
</div>
</nav>
Including the aria-label attribute
provides a brief description of the purpose of the navigation. In this case, as the value of the attribute is redundant to
text that is visible on the page, it is preferable to use aria-labelledby
to reference the visible text.
We can change the non-semantic <div> to a paragraph <p>, then add an id so it can be referenced. We then use aria-labelledby:
<nav aria-labelledby="tocTitle">
<p id="tocTitle">On this page</p>
In addition to reducing redundancy, visible text gets translated by translation services, whereas attribute values may not. When possible, if text is present that provides for an adequate label, prefer that over attribute text.
<nav aria-label="Table of Contents">
<p>On this page</p>
When providing an accessible name on an element, don't include the name of the element. Screen readers provide that information to the user.
For example, when using the <nav> element, don't include "navigation"; that information is included when using semantic elements.
The links themselves are in an unordered list. While they don't have to be nested in a list, using a list enables screen reader users to know how many list items, and therefore links, are an individual list in a navigation.
<nav aria-labelledby="tocTitle">
<p id="tocTitle">On this page</p>
<ul role="list">
<li>
<a href="#skip">Skip to content link</a>
</li>
<li>
<a href="#toc">Table of contents</a>
</li>
<li>
<a href="#bc">Page breadcrumbs</a>
</li>
<li>
<a href="#ln">Local navigation</a>
</li>
<li>
<a href="#global">Global navigation</a>
</li>
</ul>
</nav>
If your browser is less than 80em wide, the "On this page" widget is below the heading and tagline. This is done by including
two table-of-contents navigation components and hiding one or the other with CSS display: none; based on a media query.
Including two identical widgets to only show one is an anti-pattern. The extra bytes are negligible. Hiding HTML content from
all users by using CSS display: none is appropriate. The issue is that, on wide screens, the table of contents comes before #main;
and on narrower screens, the table of contents is nested within #main. Using the keyboard to skip to content skips over the table of
contents on a wide screen. While users are accustomed to content being responsive and changing location when they change devices or
increase their font size, they do not expect the tab order to change when they do so. Page layouts should be accessible, predictable,
and consistent across a site. Here, the location of the table of contents is not predictable.
Page breadcrumbs
If the site has a simple hierarchical directory structure, as is the case with web.dev, the breadcrumb navigation will often be composed of a link to the home, or hostname, with a link to the index file of each directory in the URL's pathname. The inclusion of the current page is optional and requires a little extra attention.
const url = new URL("https://web.dev/learn/html/navigation");
const sections = url.hostname + url.pathname.split('/');
// "web.dev,learn,html,navigation"
The sections of the breadcrumb show the path from the current page back to the home page, showing each level in-between.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/DNpEzCXqogU6qfJWduLo.png", alt="Breadcrumbs indicating the path to the current page.", width="300", height="75" %}
Every Learn HTML module page has the same breadcrumb navigation, displaying the hierarchy of the HTML lessons within the
learn section of web.dev. The code is similar to the following, with classes and SVG details removed for clarity:
<nav aria-label="breadcrumbs">
<ul role="list">
<li>
<a href="/">
<svg aria-label="web.dev" role="img">
<use href="#webDevLogo" />
</svg>
</a>
</li>
<li>
<a href="/learn/">Learn</a>
</li>
<li>
<a href="/learn/html">Learn HTML!</a>
</li>
</ul>
<share-action authors="@estellevw" data-action="click" data-category="web.dev" data-icon="share" data-label="share, twitter" role="button" tabindex="0">
<svg aria-label="share" role="img">
<use href="#shareIcon" />
</svg>
<span>Share</span>
</share-action>
</nav>
Between links there are CSS-generated content separators. The separators come before each list item starting with the second <li>.
[aria-label^="breadcrumb" i] li + li::before {
content: "";
display: block;
width: 8px;
height: 8px;
border-top: 2px solid currentColor;
border-right: 2px solid currentColor;
rotate: 45deg;
opacity: .8
}
Screen readers will not "see" them, which is best practice: separators between breadcrumb links should be hidden from screen readers. They must also have enough contrast against their background, the same as regular text.
This version uses an unordered list and list items. An ordered list is preferable as ordered list items are enumerated.
Also it is a list: role="list" was added back in because some CSS display property values remove the semantics from some elements.
Generally, the link to the home page in a breadcrumb should read "home" rather than be the site logo with the name of the site as the label. But as the breadcrumb is at the top of the document and is the only occurrence of the logo on the page, it makes sense as to why this anti-pattern was used.
The last element is a custom <share-action> element. Custom elements are discussed in section 15. While this custom element
is not part of the breadcrumb, including an unrelated element in a <nav> is fine, as long as the inclusion is consistent on all pages.
Current page
On this page, the current page, "Navigation", is not included in the breadcrumb. When the current page is included in a
breadcrumb, the text should preferably not be a link, and aria-current="page" should be included on the current page's
list item. When it isn't included, it is helpful to indicate that the heading that follows is the current page with an
icon or other symbol.
Should the design change, an alternative version of the breadcrumb could be used:
<nav aria-label="breadcrumbs">
<ol role="list">
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/learn/">Learn</a>
</li>
<li>
<a href="/learn/html">Learn HTML!</a>
</li>
<li aria-current="page">
Navigation
</li>
</ol>
</nav>
Local navigation
If you are visiting this site on a tablet or mobile device, or otherwise have a narrower screen, when you load this page, the sidebar is hidden. You can make it visible via the hamburger menu in the top navigation bar (yes, the header is a custom <web-header> element with role="navigation" set).
The main difference between the permanent local navigation on wide screens and the local navigation on narrower screens that can be made to appear and disappear, is the display of the close button on the version that can be hidden. This icon is hidden on wide screens with display: none;.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/MHQ1i1NnGKx2N0ovmzxy.png", alt="The local navigation is showing a checkmark next to the name of this chapter.", width="365", height="75", style="max-width: 300px; margin: 0 auto;" %}
The link to this document, section 010, has a slightly different appearance from the other links in the local navigation to indicate to sighted users that this is the current page. This visual difference is created with CSS. The current page is also identified with the aria-current="page" attribute. This informs assistive technologies that it is a link to the current page. The HTML for this list item within this local navigation is similar to:
<li>
<a aria-current="page" href="/learn/html/navigation/" data-complete="true">
<span>010</span>
<span>Navigation</span>
<svg aria-label="Check" role="img">
<use href="#checkmark" />
</svg>
</a>
</li>
If this isn't the first time you visit this page, the checkmark will not be visible. If you have visited this page in the past, the
data-complete custom attribute is set to true, and the checkmark will be displayed. The checkmark is included
in each link, but CSS hides the checkbox from users who have not been to this page before with display: none based on the absence of the
data-complete="true" attribute and value:
.course .stack-nav a:not([data-complete="true"]) svg {
display: none;
}
When display is set to something other than none, the role informs assistive technology that the inline SVG is an image,
and the aria-label acts as the alt attribute on an <img>would.
Global navigation
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/p9q003xRDvYiTidnBEHf.png", alt="A well-contrasted navigation header, with a white picker on a black background.", width="600", height="31" %}
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/HyvinG9Tf5wR5qq1tM5s.png", alt="A poorly-contrasted navigation header, with a black picker on a grey background.", width="600", height="35" %}
The content of the news and sports global navigation headers are the same, but the icon showing that the user is currently on sports does not have enough contrast to be accessible; even for visitors who don't have low vision. Both sections have a global navigation with a section-specific local navigation below it.
Similar to global navigations, footers should be identical on all pages. But that is the only similarity. Global navigation enables navigation to all parts of the site from a product perspective. Navigation elements within a footer don't have specific requirements. Generally, the footer will include corporate links, such as legal statements, about the company, and careers, and can lead to external sources, such as social media icons.
Overview of HTML
HyperText Markup Language, or HTML, is the standard markup language for describing the structure of documents displayed on the web. HTML consists of a series of elements and attributes which are used to mark up all the components of a document to structure it in a meaningful way.
HTML documents are basically a tree of nodes, including HTML elements and text nodes. HTML elements provide the semantics and formatting for documents, including creating paragraphs, lists and tables, and embedding images and form controls. Each element may have multiple attributes specified. Many elements can have content, including other elements and text. Other elements are empty, with the tag and attributes defining their function.
There are several categories of elements, including metadata, sectioning, text, inline semantic, form, interactive, media, component, and scripting. We'll cover most of these in the series. But first, what is an element?
Elements
HTML consists of a series of elements, which you use to enclose, or wrap, different parts of the content to make it appear or act in a certain way. HTML elements are delineated by tags, written using angle brackets (< and >).
Our page title is a heading, level one, for which we use the <h1> tag. The actual title, "Machine Learning Workshop", is the content of our element. The content goes between the open and closing tags. The entire thing—the opening tag, closing tag, and the content—is the element.
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/oyFzeg8ttK57XMGuVNvG.png", alt="The tags and content that make up an HTML element.", width="752", height="245" %}
The closing tag is the same tag as the opening tag, preceded by a slash.
<p>This paragraph has some
<strong><em>strongly emphasized</em></strong>
content</p>
This paragraph element has other elements nested in it. When nesting elements, it's important that they are properly nested. HTML tags should be closed in the reverse order of which they were opened. In the above example, notice how the <em> is both opened and closed within the opening and closing <strong> tags, and the <strong> is both open and closed within the <p> tags.
Browsers do not display the tags. The tags are used to interpret the content of the page.
HTML is very, very forgiving. For example, if we omit the closing </li> tags, the closing tags are implied.
<ul>
<li>Blendan Smooth
<li>Hoover Sukhdeep
<li>Toasty McToastface
</ul>
Although it is valid to not close an <li>, it isn't good practice. The closing </ul> is mandatory. If the unordered list's closing tag is omitted, the browser will try to determine where your list and list items end, but you might not agree with the decision.
The specification for each element lists whether the closing tag is mandatory or not. "Neither tag is omissible" in the specification means both an opening tag and a closing tag are required. The specification provides a list of all the required closing tags.
If the <em> or <strong> in the example earlier had not been closed, the browser may or may not close the element for you. Not closing an <em> simply leads to content possibly being rendered differently than you intended. If a </ul> is omitted and the next tag is a closing tag for the list's parent container, you're lucky. If, on the other hand, it's an opening <h1> tag, the browser will assume the header is part of the list, including inheriting styles. Some omitted closing tags cause bigger issues: not closing some tags, such as <script>, <style>, <template>, <textarea>, and <title>, breaks subsequent content as shown in the following example.
{% Codepen { user: 'web-dot-dev', id: 'VwxzBLq', height: 500, theme: 'dark', tab: 'html,result' } %}
Having some content being unintentionally italic or having a heading indented won't destroy your business. Having most of your content appear unstyled in a 200x300 textarea because you forgot to add a </textarea> or not show up at all because you forgot to close a </style> makes the site unusable.
In some cases, browsers will include elements even if the tags aren't present in the markup. Because elements can be implied, an element can exist even when the tags don't. The browser will add a <body></body> around your content and <tbody></tbody> around your table rows, even if you don't. That being said, while it is valid to omit tags, don't. Also, as already mentioned, make sure they are correctly nested. Your future self as a maintainer of markup, and anyone else working on your code base, will thank you.
There are two types of elements: replaced and non-replaced.
Non-replaced elements
The paragraph, header, and lists marked up in the earlier section are all non-replaced. Non-replaced elements have opening and (sometimes optional) closing tags that surround them and may include text and other tags as sub-elements. These enclosing tags can turn a phrase or image into a hyperlink, can make a sentence into a header, can give emphasis to words, and so on.
Replaced and void elements
Replaced elements are replaced by objects, be it a graphical user interface (UI) widget in the case of most form controls, or a raster or scalable image file in the case of most images. Being replaced by objects, each comes with a default appearance. Depending on the type of object and the browser, the applicable styles are limited. For example, most browsers enable limited styling of UI widgets and related pseudo-elements. In the case of raster images, height, width, clipping, and filtering are easily done with CSS, but not much else. On the other hand, scalable vector graphics, using a markup language based on XML similar to HTML are fully scalable (unless they contain raster images). They are also fully styleable. Note that the ability to style an embedded SVG from the CSS linked to the HTML file that embeds it depends on how the SVG is set up.
In this example, the two replaced elements <img> and <input> are replaced by non-text content: an image and a graphical user interface object, respectively.
<input type="range">
<img src="switch.svg" alt="light switch">
Output of the above HTML:
Replaced elements and void elements are often confused. Void elements are all self-closing elements and are represented by one tag. This means there is no such thing as a closing tag for a void element. Optionally, you can include a slash at the end of the tag, which many people find makes markup easier to read. Continuing with this example, we self close the tag with a slash:
<input type="range"/>
<img src="switch.svg" alt="light switch"/>
The slash at the end is old school: it's a way of indicating that the element is self-closing and there will be no matched end or closing tag.
Void elements cannot contain text content or nested elements. Void elements include <br>, <col>, <embed>, <hr>, <img>, <input>, <link>, <meta>, <source>, <track>, and <wbr>, among others.
Most replaced elements are void elements, but not all. The video, picture, object, and iframe elements are replaced, but aren't void. They can all contain other elements or text, so they all have a closing tag.
Most void elements are replaced; but again, not all, as we saw with base, link, param, and meta. Why have a void element, which can't have any content, that isn't replaced and thereby doesn't render anything to the screen? To provide information about the content! The information is provided by the elements' attributes.
Attributes
You may have noticed the <img> and <input> examples had more than just the element type in their opening tag. These extra bits of space-separated name/value pairs (though sometimes including a value is optional) are called attributes. Attributes are what make HTML so incredibly powerful. We'll be covering hundreds of attributes and attribute values in this series, but here we'll just discuss what they are in general and how to include them.
Attributes provide information about the element. The attribute, like the rest of the opening tag, won't appear in the content, but they do help define how the content will appear to both your sighted and non-sighted (assistive technologies and search engines) users.
Attributes only appear in the opening tag. The opening tag always starts with the element type. The type can be followed by zero or more attributes, separated by one or more spaces. Most attribute names are followed by an equal sign equating it with the attribute value, wrapped with opening and closing quotation marks.
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/IdGp51MB61yo6WDSRZZ3.png", alt="An opening tag with attributes.", width="800", height="218" %}
In this example, we have an anchor link with two attributes. These two attributes have converted the content "Registration" into an internal anchor link that scrolls to the attribute id="register" in the current browser tab when the link is clicked, tapped, or activated with the keyboard or other device.
Attributes define the behavior, linkages, and functionality of elements. We'll cover more attributes in the Attributes section of this series. For now, just note that some attributes are global—meaning they can appear within any element's opening tag. Some apply only to several elements but not all, and others are element-specific, relevant only to a single element.
Most attributes are name/value pairs. Boolean attributes, whose value is true, false, or the same as the name of the attribute, can be included as just the attribute: the value is not necessary.
<img src="switch.svg" alt="light switch" ismap />
This image has three attributes: src, alt, and ismap. The src attribute is used to provide the location of the SVG image asset. The alt attribute provides alternative text describing the contents of the image. The ismap attribute is Boolean, and doesn't require a value. This is just to explain what attributes are. We'll cover these attributes in more detail in the images section.
While quoting attributes isn't always required, it sometimes is. If the value includes a space or special characters, quotes are needed. For this reason, quoting is always recommended. One or more spaces between attributes if the attribute value is quoted are not actually required, but, to be safe, and for legibility, quotes and spaces are recommended, and appreciated.
HTML is not case-sensitive, but some attribute values are. Values that are defined in the specification are case-insensitive. Strings that are not defined as keywords are generally case-sensitive, including id and class values.
Note that if an attribute value is case-sensitive in HTML, it is case-sensitive when used as part of an attribute selector in CSS and in JavaScript.
To make markup easier to read, it is recommended, but not required, to mark up your HTML using lowercase letters for all your element names and attribute names within your tags, and quote all attribute values. If you ever hear the term "XHTML style markup", this, and self-closing empty elements with a slash at the end, is what that means.
Appearance of elements
The default appearance of semantic elements is set by user-agent stylesheets. Most browsers use actual stylesheets for this purpose, while others simulate them in code. The end result is the same. Although some constraints on user-agent stylesheets are set by the HTML specification, browsers have a lot of latitude, which means some differences exist between browsers.
The element you choose, and therefore the tags you use, should be appropriate for the content you are displaying, as tags have semantic meaning. The semantics, or role, of an element is important to assistive technologies and, in some cases, search engines. HTML should be used to structure content, not to define the content's appearance. Appearance is the realm of CSS. While many elements that alter the appearance of content, such as <h1>, <strong>, and <em>, have a semantic meaning, the appearance can and generally will be changed with author styles.
<h1>This header has both <strong>strong</strong> and <em>emphasized</em> content</h1>
Element, attributes, and JavaScript
The Document Object Model (DOM) is the data representation of the structure and content of the HTML document. As the browser parses HTML, it creates a JavaScript object for every element and section of text encountered. These objects are called nodes—element nodes and text nodes, respectively.
There is an interface to define the functionality of every HTML element. The HTML DOM API provides access to and control of every HTML element via the DOM, providing interfaces for the HTML element and all the HTML classes that inherit from it. The HTMLElement interface represents the HTML element and all of its descendant nodes. Every other element implements it via an interface that inherits from it. Each inheriting interface has a constructor, methods, and properties. Via the inherited HTMLElement properties, you can access every global attribute, as well as input, pointer, transition, and animation events. Via the individual element's sub-type, such as HTMLAnchorElement and HTMLImageElement, you can access element-specific attribute values and methods.
{% Assessment 'overview' %}
Semantic HTML
With over 100 HTML elements, and the ability to create custom elements, there are infinite ways to mark up your content; but some ways—notably semantically—are better than others.
Semantic means "relating to meaning". Writing semantic HTML means using HTML elements to structure your content based on each element's meaning, not its appearance.
This series hasn't covered many HTML elements yet, but even without knowing HTML, the following two code snippets show how semantic markup can give content context. Both use a word count instead of ipsum lorem to save some scrolling—use your imagination to expand "thirty words" into 30 words:
The first code snippet uses <div> and <span>, two elements with no semantic value.
<div>
<span>Three words</span>
<div>
<a>one word</a>
<a>one word</a>
<a>one word</a>
<a>one word</a>
</div>
</div>
<div>
<div>
<div>five words</div>
</div>
<div>
<div>three words</div>
<div>forty-six words</div>
<div>forty-four words</div>
</div>
<div>
<div>seven words</h2>
<div>sixty-eight words</div>
<div>forty-four words</div>
</div>
</div>
<div>
<span>five words</span>
</div>
Do you get a sense of what those words expand to? Not really.
Let's rewrite this code with semantic elements:
<header>
<h1>Three words</h1>
<nav>
<a>one word</a>
<a>one word</a>
<a>one word</a>
<a>one word</a>
</nav>
</header>
<main>
<header>
<h1>five words</h1>
</header>
<section>
<h2>three words</h2>
<p>forty-six words</p>
<p>forty-four words</p>
</section>
<section>
<h2>seven words</h2>
<p>sixty-eight words</p>
<p>forty-four words</p>
</section>
</main>
<footer>
<p>five words</p>
</footer>
Which code block conveyed meaning? Using only the non-semantic elements of <div> and <span>, you really can't tell what the content in the first code block represents. The second code example, with semantic elements, provides enough context for a non-coder to decipher the purpose and meaning without having ever encountered an HTML tag. It definitely provides enough context for the developer to understand the outline of the page, even if they don't understand the content, such as content in a foreign language.
In the second code block, we can understand the architecture even without understanding the content because semantic elements provide meaning and structure. You can tell that the first header is the site's banner, with the <h1> likely to be the site name. The footer is the site footer: the five words may be a copyright statement or business address.
Semantic markup isn't just about making markup easier for developers to read; it's mostly about making markup easy for automated tools to decipher. Developer tools demonstrate how semantic elements provide machine-readable structure as well.
Accessibility object model (AOM)
As the browser parses the content received, it builds the document object model (DOM) and the CSS object model (CSSOM). It then also builds an accessibility tree. Assistive devices, such as screen readers, use the AOM to parse and interpret content. The DOM is a tree of all the nodes in the document. The AOM is like a semantic version of the DOM.
Let's compare how both of these document structures are rendered in Firefox's accessibility panel:
In the second screen shot, there are four landmark roles in the second code block. It uses semantic landmarks conveniently named <header>, <main>, <footer>, and <nav> for "navigation". Landmarks provide structure to web content and ensure important sections of content are easily keyboard navigable for screen reader users.
Note that <header> and <footer> are landmarks, with the roles of banner and contentinfo respectively, when they are not nested in other landmarks. Chrome's AOM shows this as follows:
Looking at Chrome developer tools, you'll note a significant difference between the accessibility object model when using semantic elements versus when you don't.
It's pretty clear that semantic element usage helps accessibility, and using non-semantic elements reduces accessibility. HTML is generally, by default, accessible. Our job as developers is to both protect HTML's default accessible nature and ensure that we maximize accessibility. You can inspect the AOM in developer tools.
The role attribute
The role attribute describes the role an element has in the context of the document. The role attribute is a global attribute—meaning it is valid on all elements—defined by the ARIA specification rather than the WHATWG HTML specification, where almost everything else in this series is defined.
Semantic elements each have an implicit role, some depending on the context. As we saw in the Firefox accessibility dev tools screenshot, the top level <header>, <main>, <footer>, and <nav> were all landmarks, while the <header> nested in <main> was a section. The Chrome screenshot lists these elements' ARIA roles: <main> is main, <nav> is navigation, and <footer>, as it was the footer of the document, is contentinfo. When <header> is the header for the document, the default role is banner, which defines the section as the global site header. When a <header> or <footer> is nested within a sectioning element, it is not a landmark role. Both dev tools' screenshots show this.
Element role names are important in building the AOM. The semantics of an element, or 'role', are important to assistive technologies and, in some cases, search engines. Assistive technology users rely on semantics to navigate through and understand the meaning of content. The element's role enables a user to access the content they seek quickly and, possibly, more importantly, the role informs the screen reader user how to interact with an interactive element once it has focus.
Interactive elements, such as buttons, links, ranges, and checkboxes, all have implicit roles, all are automatically added to the keyboard tab sequence, and all have default expected user action support. The implicit role, or explicit role value, informs the user to expect element-specific default user interactions.
Using the role attribute, you can give any element a role, including a different role than the tag implies. For example, <button> has the implicit role of button. With role="button", you can turn any element semantically into a button: <p role="button">Click Me</p>.
While adding role="button" to an element will inform screen readers that the element is a button, it doesn't change the appearance or functionality of the element. The button element provides so many features without you doing any work. The button element is automatically added to the document's tab ordering sequence, meaning it is by default keyboard focusable. The Enter and Space keys both activate the button. Buttons also have all the methods and properties provided to them by the HTMLButtonElement interface. If you don't use the semantic button for your button, you have to program all those features back in. It's so much easier to just go with <button>.
Go back to the screenshot of the AOM for the non-semantic code block. You'll note that non-semantic elements don't have implicit roles. We can make the non-semantic version semantic by assigning each element a role:
<div role="banner">
<span role="heading" aria-level="1">Three words</span>
<div role="navigation">
<a>one word</a>
<a>one word</a>
<a>one word</a>
<a>one word</a>
</div>
</div>
While the role attribute can be used to add semantics to any element, you should instead use elements with the implicit role you need.
Semantic elements
Asking yourself, "Which element best represents the function of this section of markup?" will generally result in you picking the best element for the job. The element you choose, and therefore the tags you use, should be appropriate for the content you are displaying, as tags have semantic meaning.
HTML should be used to structure content, not to define content's appearance. The appearance is the realm of CSS. While some elements are defined to appear a certain way, don't use an element based on how the user agent stylesheet makes that element appear by default. Rather, select each element based on the element's semantic meaning and functionality. Coding HTML in a logical, semantic, and meaningful way helps to ensure CSS is applied as intended.
Choosing the right elements for the job as you code means you won't have to refactor or comment your HTML. If you think about using the right element for the job, you'll most often pick the right element for the job. If you don't, you probably won't. When you understand the semantics of each element and are aware of why choosing the right element is important, you will generally be able to make the right choice without much additional effort.
In the next section, you'll use the semantic elements to build your document structure.
{% Assessment 'semantic-html' %}
Tables
HTML tables are used for displaying tabular data with rows and columns. The decision to use a <table> should be based on
the content you are presenting and your users' needs in relation to that content. If data is being presented, compared, sorted,
calculated, or cross-referenced, then <table> is probably the right choice. If you simply want to lay out non-tabular content neatly,
such as a large group of thumbnail images, tables are not appropriate: instead, create a list of images and
style the grid with CSS.
In this section, we are going to discuss all the elements that make up the table, along with some accessibility and usability features you should consider as you present tabular data. While Learn HTML isn't fundamentally about CSS, and there is an entire course dedicated to learning CSS, we will cover some table-specific CSS properties.
Table elements, in order
The <table> tag wraps the table content, including all the table elements.
The implicit ARIA role of a <table> is table; assistive technologies know this element is a table structure containing data arranged in
rows and columns. If the table maintains a selection state, has two-dimensional navigation, or allows the user to rearrange cell order, set role="grid".
If the rows of the grid can be expanded and collapsed, use role="treegrid" instead.
Inside the <table>, you'll find the table headers (<thead>), table bodies (<tbody>), and, optionally, table footers (<tfoot>).
Each of these is made up of table rows (<tr>). Rows contain table header (<th>) and table data (<td>) cells which, in turn, contain all the data.
In the DOM, before any of this, you may find two additional features: the table caption (<caption>) and column groups (<colgroup>). Depending
on whether or not the <colgroup> has a span attribute, it may contain nested table column (<col>) elements.
The table's children are, in order:
<caption>element<colgroup>elements<thead>elements<tbody>elements<tfoot>elements
We'll cover the <table> elements' children, which are all optional but recommended, then take a look at rows, table header cells,
and table data cells. The <colgroup> will be covered last.
Table caption
Being a native, semantic element, <caption> is the preferred
method of giving a name to a table. The <caption> provides a descriptive, programmatically associated table title. It is
visible and available to all users by default.
The <caption> element should be the first element nested in the <table> element. Including it lets all users know the
purpose of the table immediately without having to read the surrounding text. Alternatively, you can use aria-label or aria-labelledby
on the <table> to provide an accessible name as the caption. The <caption> element has no element-specific attributes.
The caption appears outside the table. The location of the caption can be set with the CSS caption-side property, which is a
better practice than using the deprecated align attribute. This can set the caption to the top and bottom. The left and right
side positioning, with inline-start and inline-end, are not yet fully supported. Top is the default browser presentation.
{% Codepen { user: 'web-dot-dev', id: 'yLEZRyW', height: 300, theme: 'dark', tab: 'html,result' } %}
Preferably, data tables should have clear headers and a caption, and be simple enough to be almost self-explanatory. Bear in mind that
not all users have the same cognitive abilities. When the table is "making a point", or otherwise needs interpretation, provide a
brief summary of the main point or function of the table. Where that summary is placed depends on its length and complexity.
If brief, use it as the inner text of the caption. If longer, summarize it in the caption, and provide the summary in the
paragraph preceding the table, associating the two with the aria-describedby
attribute. Putting the table in a <figure> and putting the summary in the <figcaption> is another option.
Data sectioning
The content of tables is made up of up to three sections: zero or more table headers (<thead>) , table bodies (<tbody>),
and table footers (<tfoot>). All are optional, with zero or more of each being supported.
These elements don't help or hinder the accessibility of the table, but they are useful in terms of usability. They provide
styling hooks. For example, the header contents can be made sticky,
while the <tbody> contents can be made to scroll. Rows not nested in one of these three containing elements are implicitly
wrapped in a <tbody>. All three share the same implicit role rowgroup.
None of these three elements has any element-specific attributes.
What we have so far:
<table>
<caption>MLW Students</caption>
<thead></thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
The <tfoot> element was originally specified to come right after the <thead> and before the <tbody> for accessibility reasons,
which is why you may come across this non-intuitive source order in legacy codebases.
Table content
Tables can be divided into table headers, bodies, and footers, but none of these really does anything if the tables do not
contain table rows, cells, and content. Each table row, <tr> contains one or more cells. If a cell is a header cell, use <th>.
Otherwise, use <td>.
User-agent stylesheets generally display the content in a <th> table header cell as centered and bold. These default styles,
and all styling, are best controlled with CSS instead of the deprecated attributes that used to be available on individual cells,
rows, and even the <table>.
There were attributes to add padding between cells and within cells, for borders, and for text alignment. Cellpadding and cellspacing,
which define the space between the content of a cell and its border, and between the borders of adjacent cells, should be set
with the CSS border-collapse and border-spacing
properties, respectively. Border-spacing will have no effect if border-collapse: collapse is set. If border-collapse: separate;
is set, it's possible to hide empty cells completely with empty-cells: hide;. To learn more about styling tables, here's an
interactive slidedeck of table-related CSS styles.
In the examples, we've added a border on the table and each individual cell with CSS to make some features more apparent:
{% Codepen { user: 'web-dot-dev', id: 'oNymmWw', height: 300, theme: 'dark', tab: 'html,result' } %}
In this example, we have a caption, a table header, and a table body. The header has one row containing three header <th> cells,
thereby creating three columns. The body contains three rows of data: the first cell is a header cell for the row, so we use <th> instead of <td>.
The <th> cell has semantic meaning, with implicit ARIA roles of columnheader
or rowheader. It defines the cell as the header for the column or row of table cells,
depending on the value of the enumerated scope attribute. The browser will default to col or row if scope is not explicitly set.
Because we have used semantic markup, the 1956 cell has two headers: Year and Lou Minious. This association tells us that "1956" is the
"year" of graduation for "Lou Minious". In this example, as we can see the entire table, the association is visually apparent.
Using <th> provides the association even when the header column or row has scrolled out of view. We could have explicitly set
<th scope="col">Year</th> and <th scope="row">Lou Minious</th> but with a simple table like this, the enumerated default values work.
Other values for scope include rowgroup and colgroup, which are useful with complex tables.
Merging cells
Similar to MS Excel, Google Sheets, and Numbers, it is possible to join multiple cells into a single cell. This is done with HTML!
The colspan attribute is used to merge two or more adjacent cells within a single row. The rowspan attribute is used to merge cells across rows,
being placed on the cell in the top row.
{% Codepen { user: 'web-dot-dev', id: 'GRGzzEd', height: 300, theme: 'dark', tab: 'html,result' } %}
In this example, the table header contains two rows. The first header row contains three cells spanning four columns: the middle cell
has colspan="2". This merges two adjacent cells. The first and last cells include rowspan="2". This merges the cell with the cell
in the adjacent row, immediately beneath it.
The second row in the table header contains two cells; these are the cells for the second and third columns in the second row. No cell is declared for the first or last column as the cell in the first and last columns in the first row span two rows.
In cases where a cell is defined by multiple header cells with associations that cannot be set by the scope attributes alone,
include the headers attribute with a space-separated list of the associated headers. As this example is a more complex table, we explicitly define
the scope of the headers with the scope attribute. To be even clearer, we added the headers attribute to each cell.
The headers attributes were possibly not necessary in such a simple use case, but they are important to have in your toolbelt
your tables grow in complexity. Tables with complex structures, such as tables where headers or cells are merged or with more
than two levels of column or row headers, require explicit identification of associated header cells. In such complex tables, explicitly
associate each data cell with each corresponding header cell with a list of space-separated id values of all the associated headers
as the value of the headers attribute.
The headers attribute is more commonly found on <td> elements, but is also valid on <th>.
That said, complex table structures can be difficult for all users, not just screen reader users, to understand. Cognitively and in terms of screen reader support, simpler tables, with few to no spanned cells, even without adding scope and headers, are more easily understood. They're also easier to manage!
Styling tables
There are two relatively obscure elements that were briefly mentioned: the column group, <colgroup>, element
and its only descendant, the empty <col> column element. The
<colgroup> element is used to define groups of columns, or <col> elements, within a table.
If used, the column grouping should be nested in the <table>, immediately after the <caption> and before any table data.
If they span more than one column, use the span attribute.
The content outline order for a table is generally as follows, with <table> and <caption> being the two elements that
should be included:
<table>
<caption>Table Caption</caption>
<colgroup>
<col/>
</colgroup>
<thead>...
Neither <colgroup> nor <col> has semantic meaning in terms of helping to make the table more accessible, but they do allow
for limited column styling, including setting a width for the column with CSS.
<col> styles will style a column as long as there are no <td> or <th> styles that override that styling. For example, when
<colspan> is used to merge cells in some rows of a table but not all, you can't be sure that a selector such as tr > *:nth-child(8),
which selects the 8th child of every row, will highlight the 8th column in full or will highlight the 8th column for several rows,
but with a smattering of 9th and 10th column cells, depending on what row or column cells were merged.
Unfortunately, only a few properties are supported, the styles aren't inherited into the cells, and the only way of using the <col>
element in targeting cells is with a complex selector including the :has() relational selector.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/UJ6y8w7K3tReIJegOTSC.png", alt="Layered rendering of the elements used to design HTML tables.", width="518", height="403" %}
If both the <table> and the <colgroup> have a background color, the background-color of the <colgroup> will be on top.
The order of drawing is: table, column groups, columns, rowgroups, rows, with cells last and on top, as shown in the schema of table
layers. The <td> and <th> elements are not descendants of <colgroup> or <col> elements, and do not inherit their styling.
To stripe a table, CSS structural selectors come in handy. For example, tbody tr:nth-of-type(odd) {background-color: rgba(0 0 0 / 0.1);}
will add a translucent black to each odd row in the body of the table, letting any background effects that are set on <colgroup> show through.
Tables are not responsive by default. Rather, they are sized according to their content by default. Extra measures are needed to get table
layout styling to effectively work across a variety of devices. If you are changing the CSS display property for table elements,
include ARIA role attributes. While that may sound redundant, the CSS displayproperty can affect the accessibility tree in some browsers.
Presenting data
Table elements have semantic meanings that are used by assistive technologies to enable navigating through the rows and columns
without getting 'lost'. The <table> element should not be used for presentation. If you need a heading over a list, use a header
and a list. If you want to lay out content in many columns, use multi-column layout.
If you want to lay out content in a grid, use CSS grid. Only use a table for data. Think of it this way: if your data requires a spreadsheet in order to be presented at a meeting, use<table>.
If you would like to use the features available in presentation software like Keynote or Powerpoint, you probably need a description list.
While sorting table columns is the purview of JavaScript, marking up your headers to let users know the column is sortable is the purview of HTML.
Let your users know the table columns are sortable with iconography showing ascending, descending, or unsorted. The column currently sorted should
include the aria-sort attribute with the enumerated value of the sort direction.
The <caption> can politely announce updates to sort order via aria-live and a
span that is dynamically updated, and which is visible to screen reader users. As the column is sortable by clicking on the header content, the header content should be a <button>.
{% Codepen { user: 'web-dot-dev', id: 'XWYOOed', height: 300, theme: 'dark', tab: 'html,result' } %}
If you're not presenting tabular data, don't use a <table>. If you do use a table for presentation, set role="none".
Many developers use tables to lay out forms but there is no need to. But you do need to know about HTML forms, so we will cover that next.
{% Assessment 'tables' %}
Template, slot, and shadow
The benefit of web components is their re-usability: you can create a UI widget once, and reuse it multiple times. While you do need JavaScript to create web components, you don't need a JavaScript library. HTML and the associated APIs provide everything you need.
The Web Component standard is made up of three parts—HTML templates, Custom Elements, and the Shadow DOM. Combined, they enable building customized, self-contained (encapsulated), reusable elements that can be seamlessly integrated into existing applications, like all the other HTML elements we've already covered.
In this section, we'll create the <star-rating> element, a web component that allows users to rate an experience on a
scale of one to five stars. When naming a custom element, it is recommended to use all lowercase letters. Also, include a dash,
as this helps distinguish between regular and custom elements.
We'll discuss using the <template> and <slot> elements, the slot attribute, and JavaScript to create a template with
an encapsulated Shadow DOM. We'll then re-use the defined element, customizing a section of text, just
like you would any element or web component. We'll also briefly discuss using CSS from within and outside of the custom element.
The <template> element
The <template> element is used to declare fragments of HTML to be cloned and inserted into the DOM with JavaScript. The contents of the element are not rendered by default. Rather, they are instantiated using JavaScript.
<template id="star-rating-template">
<form>
<fieldset>
<legend>Rate your experience:</legend>
<rating>
<input type="radio" name="rating" value="1" aria-label="1 star" required />
<input type="radio" name="rating" value="2" aria-label="2 stars" />
<input type="radio" name="rating" value="3" aria-label="3 stars" />
<input type="radio" name="rating" value="4" aria-label="4 stars" />
<input type="radio" name="rating" value="5" aria-label="5 stars" />
</rating>
</fieldset>
<button type="reset">Reset</button>
<button type="submit">Submit</button>
</form>
</template>
As the contents of a <template> element are not written to the screen, the <form> and its contents aren't rendered.
Yes, this Codepen is blank, but if you inspect the HTML tab, you'll see the <template> markup.
{% Codepen { user: 'web-dot-dev', id: 'vYzBNKR' } %}
In this example, the <form> is not a child of a <template> in the DOM. Rather, contents of <template> elements are children
of a DocumentFragment returned by the HTMLTemplateElement.content
property. To be made visible, JavaScript must be used to grab the contents and append those contents to the DOM.
{% Codepen { user: 'web-dot-dev', id: 'WNgeQGq' } %}
This brief JavaScript did not create a custom element. Rather, this example has appended the contents of the <template> into the <body>.
The content has become part of the visible, styleable DOM.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/xd926m5ueCjzf3xt815J.png", alt="A screenshot of the previous codepen as shown in the DOM.", width="800", height="375" %}
Requiring JavaScript to implement a template for just one star rating isn't very useful, but creating a web component for a repeatedly used, customizable star rating widget is useful.
The <slot> element
We include a slot to include a customized per occurrence legend. HTML provides a <slot>
element as a placeholder inside a <template> that, if provided a name, creates a "named slot". A named slot can be used
to customize content within a web component. The <slot> element gives us a way to control where the children of a custom
element should be inserted within its shadow tree.
In our template, we change the <legend> to a <slot>:
<template id="star-rating-template">
<form>
<fieldset>
<slot name="star-rating-legend">
<legend>Rate your experience:</legend>
</slot>
The name attribute is used to assign slots to other elements if the element has a slot attribute whose value matches the
name of a named slot. If the custom element doesn't have a match for a slot, the contents of the <slot> will be rendered.
So we included a <legend> with generic content that is OK to be rendered if anyone simply includes <star-rating></star-rating>, with no content, in their HTML.
<star-rating>
<legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
<legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
<legend slot="star-rating-legend">Toasty McToastface</legend>
<p>Is this text visible?</p>
</star-rating>
The slot attribute is a global attribute that is used
to replace the contents of the <slot> within a <template>. In our custom element, the element with the slot attribute
is a <legend>. It doesn't need to be. In our template, <slot name="star-rating-legend"> will be replaced with <anyElement slot="star-rating-legend">,
where <anyElement> can be any element, even another custom element.
Undefined elements
In our <template> we used a <rating> element. This is not a custom element. Rather, it's an unknown element. Browsers
don't fail when they don't recognize an element. Unrecognized HTML elements are treated by the browser as anonymous inline
elements that can be styled with CSS. Similar to <span>, the <rating> and <star-rating> elements have no user-agent applied
styles or semantics.
{% Codepen { user: 'web-dot-dev', id: 'jOvNbwO' } %}
Note that the <template> and contents are not rendered. The <template> is a known element that contains content that
is not to be rendered. The <star-rating> element has yet to be defined. Until we define an element, the browser displays it
like all unrecognized elements. For now, the unrecognized <star-rating> is treated as an anonymous inline element, so the content
including legends and the <p> in the third <star-rating> are displayed as they would be if they were in a <span> instead.
Let's define our element to convert this unrecognized element into a custom element.
Custom elements
JavaScript is required to define custom elements. When defined, the contents of the <star-rating> element will be replaced by a
shadow root containing all the contents of the template we associate with it. The <slot> elements from the template are replaced
with the contents of the element within the <star-rating> whose slot attribute value matches the <slot>'s name value, if
there is one. If not, the contents of the template's slots are displayed.
Content within a custom element that isn't associated with a slot—the <p>Is this text visible?</p> in our third <star-rating>—is not included in
the shadow root and therefore not displayed.
We define the custom element named star-rating
by extending the HTMLElement:
customElements.define('star-rating',
class extends HTMLElement {
constructor() {
super(); // Always call super first in constructor
const starRating = document.getElementById('star-rating-template').content;
const shadowRoot = this.attachShadow({
mode: 'open'
});
shadowRoot.appendChild(starRating.cloneNode(true));
}
});
{% Codepen { user: 'web-dot-dev', id: 'poOzjpj' } %}
Now that the element is defined, every time the browser encounters a <star-rating> element, it will be rendered as defined
by the element with the #star-rating-template, which is our template. The browser will attach a shadow DOM tree to the node, appending
a clone of the template contents to that shadow DOM.
Note that the elements upon which you can attachShadow() are limited.
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));
If you take a look at the developer tools, you'll note the <form> from the <template> is part of the shadow root of each custom element.
A clone of the <template> contents is apparent in each custom element in the developer tools and visible in the browser, but the contents
of the custom element itself are not rendered to the screen.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/CqbHMMxnse6n8STAocFo.png", alt="DevTools screenshot showing the cloned template contents in each custom element.", width="800", height="842" %}
In the <template> example, we appended the template contents to the document body, adding the content to the regular DOM.
In the customElements definition, we used the same
appendChild(), but the cloned template contents were appended to an
encapsulated shadow DOM.
Notice how the stars went back to being unstyled radio buttons? Being part of a shadow DOM rather than the standard DOM, the styling within Codepen's CSS tab does not apply. That tab's CSS styles are scoped to the document, not to the shadow DOM, so the styles aren't applied. We have to create encapsulated styles to style our encapsulated Shadow DOM content.
Shadow DOM
The Shadow DOM scopes CSS styles to each shadow tree, isolating it from the rest of the document. This means external CSS doesn't apply to your component, and component styles have no effect on the rest of the document, unless we intentionally direct them to.
Because we have appended the contents to a shadow DOM, we can include a <style> element
providing encapsulated CSS to the custom element.
{% Codepen { user: 'web-dot-dev', id: 'dyqbYme' } %}
Being scoped to the custom element, we don't have to worry about styles seeping out to the rest of the document. We can
substantially reduce the specificity of the selectors. For example, as the only inputs used in the custom element are radio
buttons, we can use input instead of input[type="radio"] as a selector.
<template id="star-rating-template">
<style>
rating {
display: inline-flex;
}
input {
appearance: none;
margin: 0;
box-shadow: none;
}
input::after {
content: '\2605'; /* solid star */
font-size: 32px;
}
rating:hover input:invalid::after,
rating:focus-within input:invalid::after {
color: #888;
}
input:invalid::after,
rating:hover input:hover ~ input:invalid::after,
input:focus ~ input:invalid::after {
color: #ddd;
}
input:valid {
color: orange;
}
input:checked ~ input:not(:checked)::after {
color: #ccc;
content: '\2606'; /* hollow star */
}
</style>
<form>
<fieldset>
<slot name="star-rating-legend">
<legend>Rate your experience:</legend>
</slot>
<rating>
<input type="radio" name="rating" value="1" aria-label="1 star" required/>
<input type="radio" name="rating" value="2" aria-label="2 stars"/>
<input type="radio" name="rating" value="3" aria-label="3 stars"/>
<input type="radio" name="rating" value="4" aria-label="4 stars"/>
<input type="radio" name="rating" value="5" aria-label="5 stars"/>
</rating>
</fieldset>
<button type="reset">Reset</button>
<button type="submit">Submit</button>
</form>
</template>
While web components are encapsulated with in-<template> markup and CSS styles are scoped to the shadow DOM and hidden
from everything outside of the components, the slot content which gets rendered, the <anyElement slot="star-rating-legend">
portion of the <star-rating>, is not encapsulated.
Styling outside of the current scope
It is possible, but not simple, to style the document from within a shadow DOM and to style the contents of a shadow DOM from the global styles. The shadow boundary, where the shadow DOM ends and the regular DOM begins, can be traversed, but only very intentionally.
The shadow tree is the DOM tree inside the shadow DOM. The shadow root is the root node of the shadow tree.
The :host pseudo-class selects the <star-rating>, the shadow host element.
The shadow host is the DOM node that the shadow DOM is attached to. To target only specific versions of the host, use :host().
This will select only the shadow host elements that match the parameter passed, like a class or attribute selector. To select
all the custom elements, you can use star-rating { /* styles */ } in the global CSS, or :host(:not(#nonExistantId)) in the template styles. In terms
of specificity, the global CSS wins.
The ::slotted() pseudo-element crosses the shadow DOM boundary
from within the shadow DOM. It selects a slotted element if it matches the selector. In our example, ::slotted(legend) matches our three legends.
To target a shadow DOM from CSS in the global scope, the template needs to be edited. The part
attribute can be added to any element that you want to style. Then use the ::part() pseudo-element
to match elements within a shadow tree that match the parameter passed. The anchor or originating element for the pseudo-element is
the host, or custom element name, in this case star-rating. The parameter is the value of the part attribute.
If our template markup began as such:
<template id="star-rating-template">
<form part="formPart">
<fieldset part="fieldsetPart">
We could target the <form> and <fieldset> with:
star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }
{% Codepen { user: 'web-dot-dev', id: 'abaovjL' } %}
Part names act similarly to classes: an element can have multiple space-separated part names, and multiple elements can have the same part name.
Google has a fantastic checklist for creating custom elements. You may also want to learn about declarative shadow DOMs.
{% Assessment 'template' %}
Text basics
Similar to how your text editor provides <h1> to <h6> headings, along with a plethora of ways to format sections of text
in meaningful and visual ways, HTML provides a very similar set of semantic and non-semantic elements to make meaning of prose.
This section covers the main ways of marking up text, or text basics. We will then discuss attributes, before exploring additional ways of marking up text, such as lists, tables, and forms.
Headings, revisited
There are six section heading elements, <h1>, <h2>, <h3>, <h4>, <h5>, and <h6>, with <h1> being most important
and <h6> the least. For many years, developers were told that headings were used by browsers to outline documents.
That was originally a goal, but browsers haven't implemented outlining features. However, screen reader users do use headings
as an exploration strategy to learn about the content of the page, navigating through headings with the h key. So ensuring
that heading levels are implemented as you would outline a document makes your content accessible and is still very much encouraged.
By default, browsers style <h1> the largest, <h2> slightly smaller, with each subsequent heading level being smaller
by default. Interestingly, browsers by default also decrement the <h1> font size based on how many <article>, <aside>, <nav>, or
<section> elements it is nested in.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/ktRJF3DKyFHtfLifJXkn.png", alt="Nested H1 examples.", width="242", height="548" %}
Some user agent stylesheets include the following selectors, or similar, to style nested <h1> elements as if they were
of a less important level:
h2, :is(article, aside, nav, section) h1 {}
h3, :is(article, aside, nav, section) :is(article, aside, nav, section) h1 {}
But the Accessibility Object Model, or AOM, still reports the level of the element correctly; in this case, "heading, level 1". Note that the browser doesn't do this for other heading levels. That said, don't use heading level-based browser styling. Even though browsers don't support outlining, pretend they do; mark up your content headings as if they do. That will make your content make sense to search engines, screen readers, and future maintainers (which just might well be you).
Outside of headings, most structured text is made up of a series of paragraphs. In HTML, paragraphs are marked up with the
<p> tag; the closing tag is optional but always advised.
The #about section has a heading and a few paragraphs:
{% Codepen { user: 'web-dot-dev', id: 'jOKdjQm', height: 300, theme: dark, tab: 'html,result' } %}
This section is not a landmark as it doesn't have an accessible name. To turn this into a region, which is a landmark role, you can use aria-labelledby to provide the accessible name:
<section id="about" aria-labelledby="about_heading">
<h2 id="about_heading">What you'll learn</h2>
Only create landmarks if and when appropriate. Having too many landmarks can quickly become disorienting for screen reader users.
Quotes and citations
When marking up an article or blog post, you may want to include a quote or pull-quote, with or without a visible citation.
There are elements for these three components: <blockquote>, <q>, and <cite> for a visible citation, or the cite attribute
to provide more information for search.
The #feedback section contains a header and three reviews; these reviews are blockquotes, some of which contain quotes,
followed by a paragraph containing the quote's citation. Omitting the third review to save space, the markup is:
{% Codepen { user: 'web-dot-dev', id: 'eYKxwbV', height: 300, theme: dark, tab: 'html,result' } %}
The information about the quote author, or citation, is not part of the quote and therefore not in the <blockquote>, but comes after the quote.
While these are citations in the lay sense of the term, they are not actually citing a specific resource, so are encapsulated in a <p> paragraph element.
The citation appears over three lines, including the author's name, previous role, and professional aspiration. The <br> line break
creates a line break in a block of text. It can be used in physical addresses, in poetry, and in signature blocks. Line
breaks should not be used as a carriage return to separate paragraphs. Instead, close the prior paragraph and open a new one. Using paragraphs
for paragraphs is not only good for accessibility but enables styling. The <br> element is just a line break; it is impacted by very few CSS properties.
While we provided citation information in a paragraph following each blockquote, the quotes shown earlier are coded this way because they didn't come from an external source. If they did, the source can (should?) be cited.
If the review was pulled from a review website, book, or other work, the <cite> element could be used for the title
of a source. The content of the <cite> can be the title of a book, the name of a website or TV show, or even the name of a
computer program. The <cite> encapsulation can be used whether the source is being mentioned in passing or if the source
is being quoted or referenced. The content of the <cite> is the work, not the author.
If the quote from Blendan Smooth was taken from her offline magazine, you would write the blockquote like this:
{% Codepen { user: 'web-dot-dev', id: 'LYrqKMq', height: 300, theme: dark, tab: 'html,result' } %}
The citation element <cite> has no implicit role and should get its accessible name from its contents; don't include an aria-label.
To provide credit where credit is due when you can't make the content visible, there is the cite attribute which takes as its value the URL of the source document or message for the information quoted. This attribute is valid on both <q> and <blockquote>. While it's a URL, it is machine readable but not visible to the reader:
{% Codepen { user: 'web-dot-dev', id: 'NWzoZob', height: 300, theme: dark, tab: 'html,result' } %}
While the </p> closing tag is optional (and always recommended), the </blockquote> closing tag is always required.
Most browsers add padding to both <blockquote> inline directions and italicize <cite> content; this can be controlled with CSS. The <blockquote> does not add quotation marks, but those can be added with CSS-generated content. The <q> element does add quotes by default, using language-appropriate quotation marks.
In the #teachers section, HAL is quoted as saying, "I'm sorry <NAME REDACTED, RIP>, but I'm afraid I can't do that, ." The code for that, in English and French, is:
{% Codepen { user: 'web-dot-dev', id: 'qBKgzgg', height: 300, theme: dark, tab: 'html,result' } %}
The inline quotation element, <q>, adds language-appropriate quotes. The user-agent default styles include open-quote and close-quote generated content:
q::before {content: open-quote;}
q::after {content: close-quote;}
The lang attribute is included to let the browser know that, while base language of the page was defined as English in the <html lang="en-US"> opening tag, this paragraph of text is in a different language. This helps voice controls such as Siri, Alexa, and voiceOver use French pronunciation. It also informs the browser what type of quotes to render.
Like <blockquote>, the <q> element supports the cite attribute.
{% Codepen { user: 'web-dot-dev', id: 'jOKdjJw', height: 300, theme: dark, tab: 'html,result' } %}
HTML Entities
You may have noticed the escape sequence or "entity". Because the < is used in HTML, you have to escape it using either < or a less easy-to-remember encoding <. There are four reserved entities in HTML: <, >, &, and ". Their character references are <, >, & and " respectively.
A few other entities you will often use are © for copyright (©), ™ for Trademark (™), and for non-breaking space.
Non-breaking spaces are useful when you want to include a space between two characters or words while preventing a line break from occurring there.
There are over 2,000 named character references.
But, if needed, every single character, including emojis, has an encoded equivalent that starts with &#.
If you take a look at ToastyMcToastface's workshop review (not included in the code sample above), there are some unusual text characters:
<blockquote>Learning with Hal and Eve exceeded all of my wildest fantasies. All they did was stick a USB in. They promised that it was a brand new USB, so we know there were no viruses on it. The Russians had nothing to do with it. This has no̶̼͖ţ̘h̝̰̩͈̗i̙̪n͏̩̙͍̱̫̜̟g̢̣ͅ ̗̰͓̲̞̀t͙̀o̟̖͖̹̕ ͓̼͎̝͖̭dó̪̠͕̜ ͍̱͎͚̯̟́w̮̲̹͕͈̟͞ìth̢ ̰̳̯̮͇</blockquote>
The last sentence in this blockquote can also be written as:
This has no̶̼͖ţ̘h̝̰̩͈̗i̙̪n͏̩̙
͍̱̫̜̟g̢̣ͅ ̗̰͓̲̞̀t͙̀o̟
̖͖̹̕ ͓̼͎̝͖̭dó̪̠͕̜ ͍̱
͎͚̯̟́w̮̲̹͕͈̟͞ìth̢ ̰̳
̯̮͇
There are a few unescaped characters and a few named character references in this code mess. Because the character set is UTF-8,
the last few characters in the blockquote don't actually need to be escaped, as in this example. Only characters not supported
by the character set need to be escaped. If needed, there are many tools to enable escaping various characters,
or you can just ensure you include <meta charset="UTF-8"> in the <head>.
Even when you specify the character set as UTF-8, you still have to escape the < when you want to print that character to the screen.
Generally, you don't need to include the named character references for >, ", or &; but if you want to write a tutorial on HTML entities,
you do need to write < when teaching someone how to code a <. 😀
Oh, and that smiley emoji is 😀, but this doc is declared as UTF-8, so it isn't escaped.
{% Assessment 'text-basics' %}
Learn Images
This course is created for beginner and advanced web developers alike, covering everything from the basics of ensuring image sources are efficiently requested and rendered, to the details of how common image formats are communicated from server to client. Throughout this course you’ll learn how to achieve the smallest possible transfer sizes for your images without compromising on their quality—at least, not in a way anyone will be able to see.
You can go through the series from start to finish for a holistic understanding of how images work on the modern web, or use it as a reference for the specific concepts and markup patterns you’ll be using in your day-to-day work.
For those new to web development, check out the Learn HTML courses for the basics of using markup, Learn CSS courses for styling fundamentals, and Learn Responsive Design courses for an understanding of how images will be rendered in responsive layouts.
Here's what you'll learn:
{% include 'partials/course-index.njk' %}
So, are you ready to learn Images?
{% Aside %} Course written by Mat Marquis, with input and review from Rachel Andrew, Jake Archibald, Barry Pollard, Aaron Forinton, Eric Portis, Zach Leatherman, and K.Adam White. {% endAside %}
Automating compression and encoding
All of the syntaxes in this course—from the encoding of image data to the information-dense
markup that powers responsive images—are methods for machines to communicate with machines. You have
discovered a number of ways for a client browser to communicate its needs to a server, and a server to respond in kind.
Responsive image markup (srcset and sizes, in particular) manage to describe a shocking amount of information in relatively
few characters. For better or worse, that brevity is by design: making these syntaxes less terse, and so easier for developers
to parse, could have made them more difficult for a browser to parse. The more complexity added to a string, the more
potential there is for parser errors, or unintentional differences in behavior from one browser to another.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/24qgm41BZl5a2gstQQfi.png", alt="An automated image encoding window.", width="800", height="472" %}
However, the same trait that can make these subjects feel so intimidating can also provide you with solutions: a syntax easily read by machines is a syntax more easily written by them. You’ve almost certainly encountered many examples of automated image encoding and compression as a user of the web: any image uploaded to the web through social media platforms, content management systems (CMS), and even email clients will almost invariably pass through a system that resizes, re-encodes, and compresses them.
Likewise, whether through plugins, external libraries, standalone build process tools, or responsible use of client-side scripting, responsive image markup readily lends itself to automation.
Those are the two primary concerns around automating image performance: managing the creation of images—their encodings,
compression, and the alternate sources you’ll use to populate an srcset attribute—and generating our user-facing markup.
In this module, you’ll learn about a few common approaches to managing images as part of a modern workflow, whether as an
automated phase in your development process, through the framework or content management system that powers your site, or
almost fully abstracted away by a dedicated content delivery network.
Automating compression and encoding
You’re unlikely to find yourself in a position where you can take the time to manually determine the ideal encoding and level of compression for each individual image destined for use on a project—nor would you want to. As important as it is to keep your image transfer sizes as small as possible , fine-tuning your compression settings and re-saving alternate sources for every image asset destined for a production website would introduce a huge bottleneck in your daily work.
As you learned when reading about the various image formats and compression types, the most efficient encoding for an image will always be dictated by its content, and as you learned in Responsive Images, the alternate sizes you’ll need for your image sources will be dictated by the position those images occupy in the page layout. In a modern workflow, you’ll approach these decisions holistically rather than individually—determining a set of sensible defaults for images, to best suit the contexts in which they’re meant to be used.
When choosing encodings for a directory of photographic images, AVIF is the clear winner for quality and transfer size but has limited support, WebP provides an optimized, modern fallback, and JPEG is the most reliable default. The alternate sizes we need to produce for images meant to occupy a sidebar in a page layout will vary a great deal from images meant to occupy the entire browser viewport at our highest breakpoints. Compression settings will require an eye toward blurring and compression artifacts across multiple resulting files—leaving less room to carve every possible byte from each image in exchange for a more flexible and reliable workflow. In sum, you’ll be following the same decision making process you’ve come to understand from this course, writ large.
As for the processing itself, there are a huge number of open source image processing libraries that provide methods of converting, modifying, and editing images in batches, competing on speed, efficiency, and reliability. These processing libraries will allow you to apply encoding and compression settings to whole directories of images at once, without the need to open image editing software, and in a way that preserves your original image sources should those settings need to be adjusted on-the-fly. They’re intended to run in a range of contexts, from your local development environment to the web server itself—for example, the compression-focused ImageMin for Node.js can be extended to suit specific applications through an array of plugins, while the cross-platform ImageMagick and the Node.js based Sharp come with a staggering number of features right out of the box.
These image processing libraries make it possible for developers to build tools dedicated to seamlessly optimizing images as part of your standard development processes—ensuring that your project will always be referencing production-ready image sources with as little overhead as possible.
Local development tools and workflows
Task-runners and bundlers like Grunt, Gulp, or Webpack can be used to optimize image assets alongside other common performance-related tasks, such as minification of CSS and JavaScript. To illustrate, let’s take a relatively simple use case: a directory in your project contains a dozen photographic images, meant to be used on a public-facing website.
First, you’ll need to ensure consistent, efficient encoding for these images. As you’ve learned in the previous modules,
WebP is an efficient default for photographic images in terms of both quality and file size. WebP is well supported,
but not universally supported, so you’ll also want to include a fallback in the form of a progressive JPEG. Then,
in order to make use of the srcset attribute for efficient delivery of these assets, you’ll need to produce multiple
alternate sizes for each encoding.
While this would be a repetitive and time-consuming chore if done with image editing software, task runners like
Gulp are designed to automate exactly this sort of repetition. The gulp-responsive
plugin, which makes use of Sharp, is one option of many that all follow a similar pattern:
collecting all the files in a source directory, re-encode them, and compress them based on the same standardized "quality"
shorthand you learned about in Image Formats and Compression. The resulting files are then output to a path you define,
ready to be referenced in the src attributes of your user-facing img elements while leaving your original files intact.
const { src, dest } = require('gulp');
const respimg = require('gulp-responsive');
exports.webp = function() {
return src('./src-img/*')
.pipe(respimg({
'*': [{
quality: 70,
format: ['webp', 'jpeg'],
progressive: true
}]
}))
.pipe(dest('./img/'));
}
With a process like this in place, no harm would be done to a production environment if someone on the project inadvertently added a photograph encoded as a massive truecolor PNG to the directory containing your original image sources—regardless of the original image’s encoding, this task will produce an efficient WebP and reliable progressive JPEG fallback, and at a compression level that can be easily adjusted on-the-fly. Of course, this process also ensures that your original image files will be retained within the project’s development environment, meaning that these settings can be adjusted at any time with only the automated output overwritten.
In order to output multiple files, you pass along multiple configuration objects—all the same, apart from the addition of
a width key and a value in pixels:
const { src, dest } = require('gulp');
const respimg = require('gulp-responsive');
exports.default = function() {
return src('./src-img/*')
.pipe(respimg({
'*': [{
width: 1000,
format: ['jpeg', 'webp'],
progressive: true,
rename: { suffix: '-1000' }
},
{
width: 800,
format: ['jpeg', 'webp'],
progressive: true,
rename: { suffix: '-800' }
},
{
width: 400,
format: ['jpeg', 'webp'],
progressive: true,
rename: { suffix: '-400' },
}]
})
)
.pipe(dest('./img/'));
}
In the case of the example above, the original image (monarch.png) was more than 3.3MB. The largest file generated by this task (monarch-1000.jpeg) is approximately 150KB. The smallest, monarch-400.web, is only 32KB.
[10:30:54] Starting 'default'...
[10:30:54] gulp-responsive: monarch.png -> monarch-400.jpeg
[10:30:54] gulp-responsive: monarch.png -> monarch-800.jpeg
[10:30:54] gulp-responsive: monarch.png -> monarch-1000.jpeg
[10:30:54] gulp-responsive: monarch.png -> monarch-400.webp
[10:30:54] gulp-responsive: monarch.png -> monarch-800.webp
[10:30:54] gulp-responsive: monarch.png -> monarch-1000.webp
[10:30:54] gulp-responsive: Created 6 images (matched 1 of 1 image)
[10:30:54] Finished 'default' after 374 ms
Of course, you’ll want to carefully examine the results for visible compression artifacts, or possibly increase compression for additional savings. Since this task is non-destructive, these settings can be changed easily.
All told, in exchange for the few kilobytes you could carve away with careful manual micro-optimization, you gain a process that is not only efficient, but resilient—a tool that seamlessly applies your knowledge of high-performance image assets to an entire project, without any manual intervention.
Responsive image markup in practice
Populating srcset attributes will typically be a straightforward manual process, as the attribute really only captures
information about the configuration you’ve already done when generating your sources. In the tasks above, we’ve established
the file names and width information that our attribute will follow:
srcset="filename-1000.jpg 1000w, filename-800.jpg 800w, filename-400.jpg 400w"
Remember that the contents of the srcset attribute are descriptive, not prescriptive. There’s no harm in overloading an
srcset attribute, so long as the aspect ratio of every source is consistent. An srcset attribute can contain the URI
and width of every alternate cut generated by the server without causing any unnecessary requests, and the more candidate
sources we provide for a rendered image, the more efficiently the browser will be able to tailor requests.
As you learned in Responsive Images, you’ll want to make use of the <picture> element to seamlessly handle the WebP
or JPEG fallback pattern. In this case, you’ll be using the type attribute in concert with srcset.
<picture>
<source type="image/webp" srcset="filename-1000.webp 1000w, filename-800.webp 800w, filename-400.webp 400w">
<img srcset="filename-1000.jpg 1000w, filename-800.jpg 800w, filename-400.jpg 400w" sizes="…" alt="…">
</picture>
As you’ve learned, browsers that support WebP will recognize the contents of the type attribute, and select that <source>
element’s srcset attribute as the list of image candidates. Browsers that don’t recognize image/webp as a valid media
type will ignore this <source>, and instead use the inner <img> element’s srcset attribute.
There’s one more consideration in terms of browser support: browsers without support for any responsive image markup will
still need a fallback, or we could run the risk of a broken image in especially old browsing contexts. Because <picture>,
<source>, and srcset are all ignored in these browsers, we’ll want to specify a default source in the inner <img>’s
src attribute.
Because scaling an image downwards is visually seamless and JPEG encoding is universally supported, the largest JPEG is a sensible choice.
<picture>
<source type="image/webp" srcset="filename-1000.webp 1000w, filename-800.webp 800w, filename-400.webp 400w">
<img src="filename-1000.jpg" srcset="filename-1000.jpg 1000w, filename-800.jpg 800w, filename-400.jpg 400w" sizes="…" alt="…">
</picture>
sizes can be a little more difficult to deal with. As you've learned, sizes is necessarily contextual—you
can’t populate the attribute without knowing the amount of space the image is meant to occupy in the rendered layout. For
the most efficient possible requests, an accurate sizes attribute needs to be in our markup at the time those requests
are made by the end user, long before the styles that govern the page layout have been requested. Omitting sizes altogether
is not only a violation of the HTML specification, but results in default behavior equivalent to sizes="100vw"—informing
the browser that this image is only constrained by the viewport itself, resulting in the largest possible candidates sources
being selected.
As is the case with any particularly burdensome web development task, a number of tools have been created to abstract away
the process of hand-writing sizes attributes. respImageLint is an absolutely
essential snippet of code intended to vet your sizes attributes for accuracy and provide suggestions for improvement.
It runs as a bookmarklet—a tool you run in your browser, while pointed at the fully rendered page containing your image
elements. In a context where the browser has full understanding of the page layout, it will also have nearly pixel-perfect
awareness of the space an image is meant to occupy in that layout at every possible viewport size.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/Yr5VQ7rAATnPzHYyxcGF.png", alt="Responsive image report showing size/width mismatch.", width="800", height="861" %}
A tool for linting your sizes attributes is certainly useful, but it has even more value as a tool to generate them wholesale.
As you know, srcset and sizes syntax is intended to optimize requests for image assets in a visually seamless way. Though
not something that should ever be used in production, a default sizes placeholder value of 100vw is perfectly reasonable
while working on a page’s layout in your local development environment. Once layout styles are in place, running respImageLint
will provide you with tailored sizes attributes that you can copy and paste into your markup, at a level of detail far greater
than one written by hand:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/fst6DoueH9kN4VEV6hU4.png", alt="Responsive image report with suggested dimensions.", width="800", height="861" %}
Though image requests initiated by server-rendered markup happen too quickly for JavaScript to generate a client-side sizes attribute,
the same reasoning doesn’t apply if those requests are initiated client-side. The Lazysizes project,
for example, allows you to fully defer image requests until after the layout has been established, allowing JavaScript to generate
our sizes values for us—a huge convenience for you, and a guarantee of the most efficient possible requests for your users.
Keep in mind, however, that this approach does mean sacrificing the reliability of server-rendered markup and the speed
optimizations built into browsers, and initiating these requests only after the page has been rendered will have an outsized
negative impact on your LCP score.
Of course, if you’re already depending on a client-side rendering framework such as React or Vue, that’s a debt you’ll already
be incurring—and in those cases, using Lazysizes means your sizes attributes can be almost completely abstracted away.
Better still: as sizes="auto" on lazy loaded images gains consensus and
native implementations, Lazysizes will effectively become a polyfill for that newly standardized browser behavior.
Image formats: AVIF
AV1 Image File Format (AVIF) is an encoding based on the open source AV1 video codec. AVIF is even newer—than WebP, only supported in Chrome and Opera since 2020, Firefox in 2021, and Safari in 2022. As with WebP, AVIF aims to address every conceivable use case for raster images on the web: GIF-like animation, PNG-like transparency, and improved perceptual quality at file sizes smaller than JPEG or WebP.
So far, AVIF shows promise. A testing framework developed by Netflix—a founding member of the Alliance for Open Media, the group responsible for the development of the AV1 codec—shows significant reductions in file sizes when compared to JPEG or WebP. Additional studies by Cloudinary and Chrome's codecs team have weighed it favorably against current encoding standards.
Though tooling is relatively limited, you can and should start experimenting with AVIF today, as one of the encodings offered by Squoosh:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/TkVWHUuKwH9qauQJDNpI.png", alt="A Squoosh screenshot showing AVIF compression settings.", width="800", height="547" %}
Browser support
Now, if you find yourself wondering why we've spent so much time discussing JPEG when AVIF and WebP can offer us higher quality results and far smaller file sizes, it's because they—and any new image encoding—come with a major catch. Support for GIF, PNG, and JPEG is guaranteed across all browsers, and has been for decades. Relative to those legacy image formats, AVIF is brand new, and while support for WebP is excellent across modern browsers, it isn't a given across the entire web.
As you can imagine, a tremendous amount of time and effort has gone into the development of new image formats that aim to improve both quality and transfer size. Formats like WebP, AVIF, and JPEG XL (not supported in any browser) aim to become the unifying solution to raster images on the web, as SVG is to vectors. Others, like JPEG 2000 (only supported in Safari) were intended to satisfy all the same use cases as a baseline JPEG, but improve on compression methods to deliver a visually similar but much smaller image.
While some of these newer formats share the JPEG name, their encodings are as fundamentally dissimilar as JavaScript is to Java. A browser that doesn't support a given encoding won't be able to parse that image file at all—it's as though I instructed you to fill out your graph paper pixel grid in a language you don't understand. The browser will request the image data, attempt to parse it, and upon failing, will discard it without rendering anything at all. An image source that fails to render outside of modern browsers would be a huge point of failure for our content, and for the web at large—a broken image and wasted bandwidth to a huge number of users around the world. You shouldn't sacrifice a more resilient web for the sake of a more performant one.
For a long time, our single-minded friend <img> made it exceptionally difficult to use any new image format, no matter how promising
it seemed. Remember, <img> only supported a single source file, and was hyper-optimized to transfer that file quickly—so quickly,
in fact, that we couldn't intercept that request via JavaScript. Until recently, the only viable option was to serve all users the brand
new type of image, and request one of the “legacy" formats when the browser fired an error—incurring a second file transfer after the first one was wasted.
For that reason and more, <img> as it had existed for decades had to change. In the next module, Responsive Images, you'll learn about the
features introduced to the HTML specification to address these issues and how to use them in your day-to-day work.
Image content delivery networks
You may already be familiar with the core concept of a content delivery network (CDN): a network of distributed but interconnected servers that quickly and efficiently delivers assets to users. When a file is uploaded to a CDN provider, a duplicate will be created on the other nodes of the CDN network around the world. When a user requests a file, the data will be sent by the node geographically closest to that user, reducing latency. The distributed nature of CDNs also provides redundancy in the event of network outages or hardware failure, and load balancing to mitigate spikes in traffic.
An image CDN can provide all these benefits, with one key difference: the ability to transform and optimize the contents of an image based on strings the URL used to access it.
A user will upload a canonical, high-resolution image to the provider, which will generate a URL used to access it:
https://res.cloudinary.com/demo/image/upload/sample.jpg
Though the exact syntax used will vary from one provider to another, at a minimum all image CDNs allow you to alter a source
image's dimensions, encoding, and compression settings. Cloudinary, for example,
performs dynamic resizing of an
uploaded image through the following syntaxes: h_ followed by the numerical height in pixels, w_ followed by the width,
and a c_ value that allows you to specify detailed information about how the image should be scaled or cropped.
Any number of transforms can be applied by adding comma-separated values to the URL, prior to the filename and extension,
meaning that the uploaded image can be manipulated as-needed through the src of the img element that requests it.
<img src="https://res.cloudinary.com/demo/image/upload/w_400/sample.jpg" alt="…">
The first time a user visits the URL containing these transforms, a new version of the image proportionally scaled to a
width of 400px (w_400) is generated and sent. That newly-created file is then cached across the CDN so it can be sent
to any user requesting the same URL, rather than recreated on-demand.
Though not uncommon for image CDN providers to offer software development kits
to facilitate advanced usage and integration with various technology stacks, this predictable URL pattern alone allows us to easily
turn a single uploaded file into a viable srcset attribute without the need for any other development tooling:
<img
src="https://res.cloudinary.com/demo/image/upload/w_1000/sample.jpg 1000w"
srcset="https://res.cloudinary.com/demo/image/upload/w_1000/sample.jpg 1000w,
https://res.cloudinary.com/demo/image/upload/w_800/sample.jpg 800w,
https://res.cloudinary.com/demo/image/upload/w_600/sample.jpg 600w"
alt="…">
We're able to manually specify our desired level of compression using what should now be a familiar syntax: q_, short
for "quality," followed by the numerical shorthand for compression level:
<img src="https://res.cloudinary.com/demo/image/upload/w_400,q_60/sample.jpg" alt="…">
It's rare that you'll need to include this information manually, however, thanks to a set of incredibly powerful features provided by most image CDNs: fully automatic compression, encoding, and content negotiation.
Automated compression
The computing power image CDNs have at their disposal means they're able to offer an incredibly powerful feature: analyzing the content of an image to algorithmically determine its ideal compression level and encoding settings, just as you or I would manually fine-tune compression for each one of our images.
These algorithms automate the decisions you might make balancing file size and perceptual quality, analyzing image content for measurable signs of degradation and fine-tuning compression settings accordingly. This frequently means huge reductions in file size compared to the one-size-fits-all manual approach to compression settings.
As complex as this process might sound, implementation couldn't be much simpler: for Cloudinary, the addition of q_auto in an
image URL enables this feature:
<img src="https://res.cloudinary.com/demo/image/upload/w_1400/sample.jpg" alt="…">
<!-- 250 KB-->
<img src="https://res.cloudinary.com/demo/image/upload/w_1400,q_auto/sample.jpg" alt="…">
<!-- 134 KB-->
Automated encoding and content negotiation
Upon receiving a request for an image, image CDNs determine the most modern encoding the browser supports through the
HTTP headers sent by the browser alongside requests for assets—specifically,
the Accept header. This header indicates the encodings that the browser is able to understand, using the same
media types we would use to populate the type
attribute of a <picture> element's <source>.
For example, adding the f_auto parameter to the list of image transforms in an asset URL explicitly tells Cloudinary to
deliver the most efficient encoding the browser is able to understand:
<img src="https://res.cloudinary.com/demo/image/upload/w_1200,q_auto,f_auto/sample.jpg" alt="…">
The server then generates a version of the image with that encoding and caches the result for all subsequent users with the same
level of browser support. That response includes a Content-Type header
to explicitly inform the browser of the file's encoding, regardless of the file extension. Even though a user with a modern browser will make a
request for a file ending in .jpg, that request will be accompanied by a header informing the server that AVIF is supported, and the server
will send an AVIF encoded file along with an explicit instruction to treat it as AVIF.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/JyJi8gXuacJB0L2puHbK.png", alt="CDN user interface.", width="800", height="649" %}
The net result is a process that not only absolves you of creating alternately encoded files and of manually fine-tuning your compression settings
(or maintaining a system that does these tasks for you), but does away with the need to use <picture> and the type attribute to effectively
deliver those files to users. So, using the srcset and sizes syntaxes alone can still provide your users with images encoded as—for example—AVIF,
falling back to WebP (or JPEG-2000, for Safari alone), falling back again to the most sensible legacy encoding.
The drawbacks of using an image CDN are more logistical than technical, chief among them being cost. While it is common for image CDNs to offer feature-robust free plans for personal usage, generating image assets requires bandwidth and storage space for uploads, processing on the server to transform images, and additional space for the cached transform results—so advanced usage and high-traffic applications may require a paid plan.
Site Generators, frameworks, and CMSs
While certainly an improvement over manually saving alternate cuts of each image and hand-optimizing them through a tool like Squoosh.app, automating image compression as a step in the development process has some limitations. For one, you may not always have full control over the images used throughout a site—most user-facing images on the web are content concerns more than development concerns, uploaded by users or editors, rather than living in a repository alongside development assets like JavaScript and stylesheets.
This will typically necessitate more than one process for image management: a development-level task for the image assets used in building and maintaining a site—backgrounds, icons, logos, and so on—and another concerned with image assets generated through use of the site, such as photographs embedded in a post by an editorial team, or an avatar uploaded by a user. While the context may differ, the end goals are the same: automated encoding and compression based on settings defined by the development team.
Fortunately, the image processing libraries you’ve come to understand from your local development workflows can be used in any number of contexts. And while there can never be a one-size-fits-all approach to your responsive image markup, these systems provide sensible defaults, configuration options, and API hooks to ease their implementation.
Static Site Generators
Compared to task-runners, there’s some similarity in the way static site generators such as Jekyll or Eleventy approach images. Using these tools to produce a deployment-ready website requires management of assets, including CSS minification or transpiling and bundling of JavaScript. As you might imagine, this means these tools enable you to process image assets the same way, using many of the libraries you’ve already learned about.
The official image plugin for Eleventy uses Sharp to provide resizing, generation of multiple source sizes, re-encoding, and compression, just like some of the tasks you’ve learned about here.
Unlike a task-runner, a static site generator has direct insight into both the configuration and usage of those libraries, and the markup being generated for the production site—meaning it can do a great deal more to automate our responsive image markup. For example, when invoked as part of a shortcode for displaying images, this plugin will output HTML according to the configuration options passed along to Sharp.
const Image = require("@11ty/eleventy-img");
module.exports = function(eleventyConfig) {
async function imageShortcode(src, alt, sizes="100vw") {
let metadata = await Image(src, {
formats: ["avif", "webp", "jpeg"],
widths: [1000, 800, 400],
outputDir: "_dist/img/",
filenameFormat: function( id, src, width, format, options ) {
const ext = path.extname( src ),
name = path.basename( src, ext );
return `${name}-${width}.${format}`
}
});
let imageAttributes = {
alt,
sizes,
loading: "lazy"
};
return Image.generateHTML(metadata, imageAttributes);
}
eleventyConfig.addAsyncShortcode("respimg", imageShortcode);
};
This shortcode could then be used in place of the default image syntax:
{% respimg "img/butterfly.jpg", "Alt attribute.", "(min-width: 30em) 800px, 80vw" %}
If configured to output multiple encodings, as above, the generated markup will be a <picture> element containing
corresponding <source> elements, type attributes, and srcset attributes already fully populated with a list of
generated candidate sizes.
<picture><source type="image/avif" srcset="/img/butterfly-400.avif 400w, /img/butterfly-800.avif 800w, /img/butterfly-1000.avif 1000w" sizes="(min-width: 30em) 800px, 80vw"><source type="image/webp" srcset="/img/butterfly-400.webp 400w, /img/butterfly-800.webp 800w, /img/butterfly-1000.webp 1000w" sizes="(min-width: 30em) 800px, 80vw"><source type="image/jpeg" srcset="/img/butterfly-400.jpeg 400w, /img/butterfly-800.jpeg 800w, /img/butterfly-1000.jpeg 1000w" sizes="(min-width: 30em) 800px, 80vw"><img alt="Alt attribute." loading="lazy" src="/img/butterfly-400.jpeg" width="1000" height="846"></picture>
Of course, this plugin won’t be able to generate a viable sizes attribute, as it can’t know the ultimate size and position
of the image in the rendered layout, but it does accept one as input when generating your markup—another job for RespImageLint.
Frameworks
Client-side rendering frameworks will require a task-runner or bundler like Webpack to edit, encode, and compress image assets
themselves. Responsive-loader, for example, also uses the Sharp library to re-save image assets. It then allows you to
then import your images as objects:
import imageAVIF from 'img/butterfly.jpg?sizes[]=400,sizes[]=800,sizes[]=1000&format=avif';
import imageWebP from 'img/butterfly.jpg?sizes[]=400,sizes[]=800,sizes[]=1000&format=webp';
import imageDefault from 'img/butterfly.jpg?sizes[]=400,sizes[]=800,sizes[]=1000';
These imported images can then be used through abstractions like React's Image component, or to populate your responsive image markup directly:
<picture>
<source type='image/avif' srcSet={imageAVIF.srcSet} sizes='…' />
<source type='image/webp' srcSet={imageWebp.srcSet} sizes='…' />
<img
src={imageDefault.src}
srcSet={imageDefault.srcSet}
width={imageDefault.width}
height={imageDefault.height}
sizes='…'
loading="lazy"
/>
A framework that does client side rendering is a strong candidate for Lazysizes and sizes="auto"—giving you almost fully
automated responsive images.
Content Management Systems
WordPress was one of the earliest adopters of native responsive images markup, and the API has been gradually improved since being introduced in WordPress 4.4 with support for WebP and control over the output mime type. WordPress core is designed to make use of the ImageMagick PHP extension (or, absent that, the GD library).
When an image is uploaded through the WordPress admin interface, that source image is used to generate user-facing files on
the server, in much the same way as you would on your local machine. By default, any image output by WordPress will come
with a generated srcset attribute based on the image sizes configured in your theme.
Two key settings that can be configured for generated images are the compression quality and the output mime type.
For example, to set the default compression quality to 70 for all generated images, use the following:
add_filter( 'wp_editor_set_quality', function() { return 70; } );
For even better compression, switch the output format for uploaded JPEG images to WebP with the following:
add_filter( 'image_editor_output_format', function( $mappings ) {
$mappings[ 'image/jpeg' ] = 'image/webp';
return $mappings;
} );
Given that WordPress has full understanding of all alternate cuts
and encodings it generates from an uploaded image, it can provide helper functions like
wp_get_attachment_image_srcset() to
retrieve the full, generated srcset value of an image attachment.
As you'll likely have guessed by this point, working with the sizes attribute is a little more fraught. Absent any information
about how images will be used in a layout, WordPress currently defaults to a sizes value that effectively says "this image
should occupy 100% of the available viewport, up to the largest source's intrinsic size"—a predictable default, but not a correct
one for any real-world application. Be sure to make use of wp_calculate_image_sizes()
to set contextually-appropriate sizes attributes in your templates.
Of course, there are countless WordPress plugins dedicated to making modern image workflows faster for development teams and users alike.
Perhaps most excitingly, plugins like Jetpack's Site Accelerator (formerly "Photon")
can provide server-side negotiation for encodings, ensuring that users will receive the smallest, most efficient encoding that their
browser is able to support without the need for <picture> and type markup pattern. It does this through use of an image content
delivery network—a technology you can make use of yourself, independent of your CMS.
All of this is also true of hosted CMS solutions like Shopify, though the mechanisms themselves will differ somewhat: offering similar
hooks for generating alternate image sources and corresponding srcset attributes
and art direction through the <picture> element.
Conclusion
Congratulations, you've made it to the end! By now you’ve learned a lot about how images work on the web, and the part that they—and you—can play in building a faster, more efficient experience for users everywhere.
Additional resources
- HTTP Archive’s annual state of the web report
- web.dev - Lazy loading images
- web.dev - Learn Accessibility: Images
- MDN - SVG
- MDN - Image file type and format guide
- MDN - Responsive Images
Descriptive syntaxes
In this module, you'll learn how to give the browser a choice of images so that it can make the best decisions about what to display. srcset
isn't a method for swapping image sources at specific breakpoints, and it isn't meant to swap one image for another. These syntaxes allow the
browser to solve a very difficult problem, independent of us: seamlessly requesting and rendering an image source tailored to a user's browsing context,
including viewport size, display density, user preferences, bandwidth, and countless other factors.
It's a big ask—certainly more than we want to consider when we're simply marking up an image for the web, and doing it well involves more information than we can access.
Describing density with x
An <img> with a fixed width will occupy the same amount of the viewport in any browsing context, regardless of the density of a user's
display—the number of physical pixels that make their screen. For example, an image with an inherent width of 400px will occupy almost
the entire browser viewport on both the original Google Pixel and much newer Pixel 6 Pro—both devices have a normalized 412px
logical pixel wide viewport.
The Pixel 6 Pro has a much sharper display, however: the 6 Pro has a physical resolution of 1440 × 3120 pixels, while the Pixel is 1080 × 1920 pixels—that is, the number of hardware pixels that make up the screen itself.
The ratio between a device's logical pixels and physical pixels is the device pixel ratio for that display (DPR). DPR is calculated by dividing the device's actual screen resolution by a viewport's CSS pixels.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/2YTBM6TXRoEE2Q3XbFhX.png", alt="A DPR of 2 displayed in a console window.", width="800", height="277" %}
So, the original Pixel has a DPR of 2.6, while the Pixel 6 Pro has a DPR of 3.5.
The iPhone 4, the first device with a DPR greater than 1, reports a device pixel ratio of 2—the physical resolution of the screen is double the logical resolution. Any device prior to the iPhone 4 had a DPR of 1: one logical pixel to one physical pixel.
If you view that 400px-wide image on a display with a DPR of 2, each logical pixel is being rendered across four of the
display's physical pixels: two horizontal and two vertical. The image doesn't benefit from the high-density display—it will look the
same as it would on a display with a DPR of 1. Of course, anything "drawn" by the browser's rendering engine—text, CSS shapes, or SVGs,
for example—will be drawn to suit the higher-density display. But as you learned from Image Formats and Compression, raster images are fixed
grids of pixels. While it may not always be glaringly obvious, a raster image upscaled to suit a higher-density display will look
low-resolution compared to the surrounding page.
In order to prevent this upscaling, the image being rendered has to have an intrinsic width of at least 800 pixels. When scaled down
to fit a space in a layout 400 logical pixels wide, that 800-pixel image source has double the pixel density—on a display with a DPR of 2,
it'll look nice and sharp.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/IoJjwpreGIT7LwRThaWn.png", alt="Close up of a flower petal showing disparity in density.", width="800", height="417" %}
{% Codepen { user: 'web-dot-dev', id: 'QWBGVyo', height: 300, theme: dark, tab: 'html,css,result' } %}
Since a display with a DPR of 1 can't make use of the increased density of an image, it will be downscaled to match the
display—and as you know, a downscaled image will look just fine. On a low-density display, an image suitable for higher-density
displays will look like any other low-density image.
As you learned in Images and Performance, a user with a low-density display viewing an image source scaled down to 400px
will only need a source with an inherent width of 400px. While a much larger image would work for all users visually, a huge,
high resolution image source rendered on a small, low density display will look like any other small, low density image, but feel far slower.
As you might guess, mobile devices with a DPR of 1 are vanishingly rare, though it is still common in "desktop" browsing contexts. According to data shared by Matt Hobbs, approximately 18% of GOV.UK browsing sessions from November 2022 report a DPR of 1. While high-density images would look the way those users might expect, they'll come at a much higher bandwidth and processing cost—of particular concern to users on the older and less powerful devices still likely to have low-density displays.
Using srcset ensures that only devices with high-resolution displays receive image sources large enough to look sharp, without passing that same
bandwidth cost along to users with lower-resolution displays.
The srcset attribute identifies one or more comma-separated candidates for rendering an image. Each candidate is made up of
two things: a URL, just like you would use in src, and a syntax that describes that image source. Each candidate in srcset
is described by its inherent width ("w syntax") or intended density ("x syntax").
The x syntax is a shorthand for "this source is appropriate for a display with this density"—a candidate followed by 2x is
appropriate for a display with a DPR of 2.
<img src="low-density.jpg" srcset="double-density.jpg 2x" alt="...">
Browsers that support srcset will be presented with two candidates: double-density.jpg, which 2x describes as appropriate
for displays with a DPR of 2, and low-density.jpg in the src attribute—the candidate selected if nothing more appropriate is
found in srcset. For browsers without support for srcset, the attribute and its contents will be ignored—the contents of src
will be requested, as usual.
It's easy to mistake the values specified in the srcset attribute for instructions. That 2x informs the browser that the
associated source file would be suitable for use on a display with a DPR of 2—information about the source itself. It doesn't tell
the browser how to use that source, just informs the browser how the source could be used. It's a subtle but important distinction: this
is a double density image, not an image for use on a double density display.
The difference between a syntax that says "this source is appropriate for 2x displays" and one that says "use this source on 2x displays"
is slight in print, but display density is only one of a huge number of interlinked factors that the browser uses to decide on the candidate
to render, only some of which you can know. For example: individually, it's possible for you to determine that a user has enabled a
bandwidth-saving browser preference through the prefers-reduced-data media query, and use that to always opt users into low-density images
regardless of their display density—but unless implemented consistently, by every developer, on every website, it wouldn't be of much use to a user.
They might have their preference respected on one site, and run into a bandwidth-obliterating wall of images on the next.
The deliberately vague resource selection algorithm used by srcset/sizes leaves room for browsers to decide to select lower density
images with bandwidth dips, or based on a preference to minimize data usage, without us taking on responsibility for how, or when, or at
what threshold. There's no sense in taking on responsibilities—and additional work—that the browser is better equipped to handle for you.
Describing widths with w
srcset accepts a second type of descriptor for image source candidates. It's a far more powerful one—and for our purposes, a
great deal easier to understand. Rather than flagging a candidate as having the appropriate dimensions for a given display density,
the w syntax describes the inherent width of each candidate source. Again, each candidate is identical save for their dimensions—the same
content, the same cropping, and the same aspect ratio. But in this case, you want the user's browser to choose between two candidates:
small.jpg, a source with an inherent width of 600px, and large.jpg, a source with an inherent width of 1200px.
srcset="small.jpg 600w, large.jpg 1200w"
This doesn't tell the browser what to do with this information—just supplies it with a list of candidates for displaying the image.
Before the browser can make a decision about which source to render, you need to provide it with a little more information: a
description of how the image will be rendered on the page. To do that, use the sizes attribute.
Describing usage with sizes
Browsers are incredibly performant when it comes to transferring images. Requests for image assets will be initiated long before requests for stylesheets or JavaScript—oftentimes even before the markup has been fully parsed. When the browser makes these requests, it has no information about the page itself, apart from the markup—it may not have even initiated requests for external stylesheets yet, let alone applied them. At the time the browser parses your markup and starts making external requests, it only has browser-level information: the size of the user's viewport, the pixel density of the user's display, user preferences, and so on.
This doesn't tell us anything about how an image is intended to be rendered in the page layout—it can't even use the viewport
as a proxy for the upper bound of the img size, as it may occupy a horizontally scrolling container. So we need to
provide the browser with this information and do it using markup. That is all we'll be able to use for these requests.
Like srcset, sizes is intended to make information about an image available as soon as the markup is parsed. Just as the srcset
attribute is shorthand for "here are the source files and their inherent sizes," the sizes attribute is shorthand for "here
is the size of the rendered image in the layout." The way you describe the image is relative to the viewport—again, viewport
size is the only layout information the browser has when the image request is made.
That may sound a little convoluted in print, but it's far easier to understand in practice:
<img
sizes="80vw"
srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
src="fallback.jpg"
alt="...">
Here, this sizes value informs the browser that the space in our layout that the img occupies has a width of 80vw—80% of
the viewport. Remember, this isn't an instruction, but a description of the image's size in the page layout. It doesn't say "make this
image occupy 80% of the viewport," but "this image will end up occupying 80% of the viewport once the page has rendered."
{% Codepen { user: 'web-dot-dev', id: 'PoBWLYP', height: 500, theme: dark, tab: 'html,result' } %}
As a developer, your job is done. You've accurately described a list of candidate sources in srcset and the width of your image
in sizes, and, just as with the x syntax in srcset, the rest is up to the browser.
But in the interest of fully understanding how this information is used, let's take a moment to walk through the decisions that a user's browser makes upon encountering this markup:
You've informed the browser that this image will take up 80% of the available viewport—so, if we were to render this img on a
device with a 1000-pixel-wide viewport, this image will occupy 800 pixels. The browser will then take that value and divide against
it the widths of each of the image source candidates we specified in srcset. The smallest source has an inherent size of 600 pixels,
so: 600÷800=.75. Our medium image is 1200 pixels wide: 1200÷800=1.5. Our largest image is 2000 pixels wide: 2000÷800=2.5.
The results of those calculations (.75, 1.5, and 2.5) are, effectively, DPR options specifically tailored to the user's
viewport size. Since the browser also has information on the user's display density at hand, it makes a series of decisions:
At this viewport size, the small.jpg candidate is discarded regardless of the user's display density—with a calculated DPR lower
than 1, this source would require upscaling for any user, so it isn't appropriate. On a device with a DPR of 1, medium.jpg provides the
closest match—that source is appropriate for display at a DPR of 1.5, so it is a little larger than necessary, but remember that downscaling is
a visually seamless process. On a device with a DPR of 2,large.jpg is the closest match, so it gets selected.
If the same image is rendered on a 600 pixel wide viewport, the result of all that math would be completely different: 80vw is now 480px.
When we divide our sources' widths against that, we get 1.25, 2.5, and 4.1666666667. At this viewport size, small.jpg will be chosen
on 1x devices, and medium.jpg will match on 2x devices.
This image will look identical in all of these browsing contexts: all our source files are exactly the same apart from their dimensions,
and each one is being rendered as sharply as the user's display density will allow. However, instead of serving large.jpg to every user
in order to accommodate the largest viewports and the highest density displays, users will always be served the smallest suitable candidate.
By using a descriptive syntax rather than a prescriptive one, you don't need to manually set breakpoints and consider future viewports and
DPRs—you simply supply the browser with information and allow it to determine the answers for you.
Because our sizes value is relative to the viewport and completely independent of the page layout, it adds a layer of complication.
It's rare to have an image that only occupies a percentage of the viewport, without any fixed-width margins, padding, or influence
from other elements on the page. You'll frequently need to express the width of an image using a combination of units; percentages, em, px, and so on.
Fortunately, you can use calc() here—any browser with native support for responsive images will support calc() as well, allowing us to
mix-and-match CSS units—for example, an image that occupies the full width of the user's viewport, minus a 1em margin on either side:
<img
sizes="calc(100vw-2em)"
srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1600w, x-large.jpg 2400w"
src="fallback.jpg"
alt="...">
Describing breakpoints
If you've spent much time working with responsive layouts, you've likely noticed something missing from these examples:
the space an image occupies in a layout is very likely to change across our layout's breakpoints. In that case, you need
to pass a little more detail along to the browser: sizes accepts a comma-separated set of candidates for the rendered size of the
image, just like srcset accepts comma-separated candidates for image sources. Those conditions use the familiar media query syntax.
This syntax is first-match: as soon as a media condition matches, the browser stops parsing the sizes attribute, and the value
specified is applied.
Say you have an image meant to occupy 80% of the viewport, minus one em of padding on either side, on viewports above 1200px—on
smaller viewports, it occupies the full width of the viewport.
<img
sizes="(min-width: 1200px) calc(80vw - 2em), 100vw"
srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
src="fallback.jpg"
alt="...">
{% Codepen { user: 'web-dot-dev', id: 'RwBoYRx', height: 500, theme: dark, tab: 'html,result' } %}
If the user's viewport is greater than 1200px, calc(80vw - 2em) describes the width of the image in our layout. If the
(min-width: 1200px) condition doesn't match, the browser moves on to the next value. Because there isn't a specific
media condition tied to this value, 100vw is used as a default. If you were to write this sizes attribute using
max-width media queries:
<img
sizes="(max-width: 1200px) 100vw, calc(80vw - 2em)"
srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
src="fallback.jpg"
alt="...">
{% Codepen { user: 'web-dot-dev', id: 'BaPQOzO', height: 500, theme: dark, tab: 'html,result' } %}
In plain language: "does (max-width: 1200px) match? If not, move on. The next value—calc(80vw - 2em)—has no qualifying condition,
so this is the one selected.
Now that you've provided the browser with all this information about your img element—potential sources, inherent widths,
and how you intend to present the image to the user—the browser uses a fuzzy set of rules for determining what to do with
that information. If that sounds vague, well, that's because it is—by design. The source-selection algorithm encoded in the
HTML specification is explicitly vague on how a source should be chosen. Once the sources, their descriptors, and how
the image will be rendered has all been parsed, the browser is free to do whatever it wants—you can't know for certain which
source the browser will choose.
A syntax that says "use this source on a high-resolution display" would be predictable, but it wouldn't address the core problem with images in a responsive layout: conserving user bandwidth. A screen's pixel density is only tangentially related to internet connection speed, if at all. If you are using a top-of-the-line laptop, but browsing the web by way of a metered connection, tethered to your phone, or using a shaky airplane wifi connection—you might want to opt out of high-resolution image sources, regardless of the quality of your display.
Leaving the final say to the browser allows for far more performance improvements than we could manage with a strictly prescriptive
syntax. For example: in most browsers, an img using the srcset or sizes syntax will never bother requesting a source with smaller
dimensions than one that the user already has in their browser's cache. What would be the point in making a new request for a source
that would look identical, when the browser can seamlessly downscale the image source it already has? But if the user scales their
viewport up to the point where a new image is needed in order to avoid upscaling, that request will still get made, so everything
looks the way you expect.
That lack of explicit control can sound a little scary at face value, but because you're using source files with identical
content, we're no more likely to present users with a "broken" experience than we would with a single-source src, regardless of
the decisions made by the browser.
Using sizes and srcset
This is a lot of information—both for you, the reader, and for the browser. srcset and sizes are both dense syntaxes,
describing a shocking amount of information in relatively few characters. That is, for better or worse, by design: making
these syntaxes less terse—and more easily parsed by us humans—could have made them more difficult for a browser to parse. The
more complexity added to a string, the more potential there is for parser errors or unintentional differences in behavior
from one browser to another. There's an upside here, however: a syntax more easily read by machines is a syntax more easily written
by them.
srcset is a clear-cut case for automation. It's rare that you'll be hand-crafting multiple versions of your images for a
production environment, instead automating the process using a task runner like Gulp, a bundler like Webpack, a third-party
CDN like Cloudinary, or functionality already built into your CMS of choice. Given enough information to generate our sources
in the first place, a system would have enough information to write them into a viable srcset attribute.
sizes is a little more difficult to automate. As you know, the only way a system can calculate the size of an image in a
rendered layout is to have rendered the layout. Fortunately, a number of developer tools have popped up to abstract away
the process of hand-writing sizes attributes—with an efficiency you could never match by hand.
respImageLint, for example, is a snippet of code intended to vet your sizes attributes
for accuracy and provide suggestions for improvement. The Lazysizes project compromises
some speed for efficiency by deferring image requests until after the layout has been established, allowing JavaScript to
generate sizes values for you. If you're using a fully client-side rendering framework such as React or Vue, there are a
number of solutions for authoring and/or generating srcset and sizes attributes, which we'll discuss further in CMS and Frameworks.
Image formats: GIF
While not terribly useful on the modern web, GIF (Graphics Interchange Format) provides a solid introduction to the core concepts of image encoding.
GIF can be thought of as a wrapper for image data. It has a viewport, of sorts, called a "logical screen," to which individual frames of image data are drawn—a bit like layers in a Photoshop document. That's how GIF supports its flipbook-like animation: a single frame is drawn to the logical screen, then replaced by another, then another. Of course, this distinction isn't important when we're dealing with a static GIF, made up of a single frame drawn to the logical screen.
GIF uses a lossless data compression method—a variant of the "Lempel–Ziv–Welch" algorithm, if you're curious. The finer details of how this algorithm works are more than we need to get into here, but at a high level: it works a bit like "Uglifying" JavaScript, where repeated strings of characters throughout the file are saved to a sort of internal dictionary, so they can be referenced rather than repeated every time they appear.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/AWgIX677XevD9J0ZUBo3.png", alt="Visualization of the gif reference using a four-by-four grid.", width="800", height="873" %}
Granted, the algorithm isn't quite as simple as a paint-by-number. It steps again through the generated table of color codes to find repeated sequences of pixel colors and creates a second table of referencable codes. At no point is any image data lost, however—just sorted and reorganized in a way that can be read without fundamentally changing it.
While GIF technically uses lossless compression, it does have a major limitation that severely impacts the quality of the images: saving an image as a GIF will always result in reduced fidelity, unless the image already uses 256 colors or less.
Each frame drawn to the GIF's logical screen can only contain a maximum of 256 colors. GIF also supports “index transparency,” where a transparent pixel will reference the index of a transparent “color” in the color table.
The practice of reducing a range of values to a smaller, approximated set of output values is called quantization, a term you'll be seeing a lot when learning about image encodings. The results of this palette quantization are usually obvious:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/PjWX2BD4QXqj3efvX7MP.png", alt="Static gif example", width="393", height="399" %}
To better understand this process, think back to the raster image grid you were able to recreate from my description.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/pwUWoFFo7MVq58e05NnK.png", alt="Three horizontal blue boxes followed by one red box", width="652", height="204" %}
This time around, add a little more detail to that original image: a few more pixels, one of which is a slightly darker shade of blue:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/m9BkyRb7yy1zLBlvlHka.png", alt="Blue to red horizontal boxes in a two-by-four configuration, with one blue box shaded darker than the others.", width="752", height="400" %}
Absent any compression—so to speak—you could describe this grid as:
Row one, column one is #0000FF. Row one, column two is #0000FF. Row one, column three is #0000FF. Row one, column four is #FF0000. Row two, column one is #0000FF. Row two, column two is #000085. Row two, column three is #0000FF. Row two, column four is #FF0000.
Using something akin to GIF's lossless data compression and color indexing, you might describe it as:
A: #0000FF, B: #FF0000, C: #000085. Row one, columns one through three are A. Row one, column four is B. Row two, column one is A. Row two, column two is C. Row two, column three is A. Row two, column four is B.
This manages to condense the pixel-by-pixel description in a few places (“columns one through three are…”), and saves a few characters by defining the repeated colors in a dictionary, of sorts, up front. There's no change to the visual fidelity. The information has been compressed without any loss.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/nNmCqD5iYi1QEEbDeL4M.png", alt="Blue to red horizontal boxes, with a single dark pixel at 2x2.", width="752", height="400" %}
As you can see, however, the single dark blue pixel is having an outsized impact on the size of our encoding. If I were to limit myself to a quantized color palette, it could be reduced much further:
A: #0000FF, B: #FF0000. Row one, columns one through three are A. Row one, column four is B. Row two, columns one through three are A. Row two, column four is B.
The unfortunate end result of those saved bytes is that you've lost pixel-perfection.
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/YhCa9QIzzkZsyw4zyUzu.png", alt="Uniformly blue to red horizontal boxes.", width="800", height="421" %}
Of course, you, the rendering engine, don't know that—the detail of the darker blue pixel was left out of how I encoded my source image. You've rendered the image exactly as I've encoded it, based on our shared understanding of the colors we have at hand.
Now, in this exaggerated example, reducing three colors to two makes for an obvious difference in quality. Across a larger and more detailed image the effects might not be quite as noticeable, but they would still be visible.
When encoded as a GIF, subtle gradients like shadows become mottled, with individual pixels standing out from their surroundings:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/ncTFXA6KsRGAK7Rdvyoi.png", alt="Pink flowers on a green background.", width="800", height="595" %}
In practice, the combination of lossless compression and palette quantization means that GIF isn't very useful in modern web development. Lossless compression doesn't do enough to reduce file sizes, and a reduced palette means an obvious reduction in quality.
Ultimately, GIF is only an efficient format for encoding simple images that already use limited color palettes, hard edges rather than anti-aliasing, and solid colors rather than gradients—all use cases that are far better served by other formats. The smaller and more featured PNG is often a better choice for raster images, though both are far inferior to SVG in terms of file size and visual fidelity for use cases like icons or line art, where vector shines. The most commonly seen modern use case for GIF is animation, but there are far more efficient—and accessible—modern video formats to serve that purpose.
A brief history of images on the web
No matter how far along you are in learning to design and develop for the web, the <img> element needs very little introduction.
Launched in Netscape (“Mosaic,” at the time) in 1993 and added to the HTML
specification in 1995, <img> has long played a simple but powerful role within the web platform. The developer adds a "source" image
file with the src attribute and provides a text alternative with the alt attribute in the event that the image cannot be rendered or
assistive technologies request an alternative. From there, the browser has only one job: get the image data, then render it as quickly as possible.
{% Codepen { user: 'web-dot-dev', id: 'jOpVPJa', height: 300, theme: dark, tab: 'html,result' } %}
For most of web development history, working with images didn't get much more complicated than that. And, despite the modern web's complexity, the fundamentals of working with images haven't changed: use a web-friendly image format for compatibility, sensible compression to conserve bandwidth, and dimensions that suit the space the image is going to occupy in the page's layout.
Using fixed-width layouts, like we did back when we thought we had more say in how users experienced the web, made this an uncomplicated process. It was particularly easy to set the size of the image source. For an image that occupied a space five hundred pixels wide and three hundred pixels tall, it was a case of specifying a source image at that same size.
Images in a responsive layout
Alongside a flexible layout and use of CSS media queries, "flexible images and media" are one of the three defining facets
of responsive web design. To make an image flexible, developers began using CSS to set max-width: 100% on the image
(or all images, site-wide) to tell the browser's rendering engine to prevent an image from ever overflowing its parent container by
scaling it down. Visually, this works perfectly–downscaling a raster image is visually seamless. With a line or two of CSS,
a scaled-down image will always look as though we've specified an image source that was meant to be displayed at that size.
When rendering engines are given more image data than necessary for the space the image occupies in a layout, they are able
to make informed decisions about how to render the reduced image, and can do so without introducing any visual artifacts or blurring.
{% Codepen { user: 'web-dot-dev', id: 'YzjpXBP', height: 500, theme: dark, tab: 'result' } %}
You wouldn't typically want to upscale an image—that is, render the <img> at a size larger than the intrinsic size of the source image.
The displayed image would appear blurry and grainy-looking.
{% Codepen { user: 'web-dot-dev', id: 'XWBNbOx', height: 500, theme: dark, tab: 'result' } %}
Using img { max-width: 100% } means that as a flexible container resizes, images will be downscaled as appropriate.
Unlike setting a more rigid width: 100%, this also ensures that the image won't be scaled up beyond its intrinsic size.
For a long time, that was it for the rules of working with images: use a format browsers understand, use a sensible level of
compression, and never scale images upwards.
{% Codepen { user: 'web-dot-dev', id: 'gOjLpEM', height: 500, theme: dark, tab: 'css,result' } %}
But as simple and effective as this approach was visually, it came at a huge performance cost. As <img> only supported a
single source for the image data, this approach required us to provide an image asset with an intrinsic size as large as
the largest size at which it could be displayed. An image meant to occupy a space in a layout that could be anywhere
from 300px to 2000px wide, depending on the user's viewport size, required an image source with an intrinsic width of
at least 2000px. For a user who only views the page by way of a small viewport, everything would look as expected—the
image would scale just fine. In the rendered page, a massive but scaled-down source image would look no different from an
appropriately-sized one. However, they would still be transferring and rendering a 2000px wide image, burning through a huge
amount of bandwidth and processing power with no tangible benefit.
Things got much worse with the advent of the first "Retina" devices, as display density became a concern alongside viewport size. An image source needs a much larger intrinsic width in order to suit a high density display. In simple terms, a display with double the density requires twice as many image pixels to render the image as sharply as possible.
Here, developers were again able to rely on rendering engines' ability to downscale images visually. By providing the browser with
an 800px wide source image in src, then specifying that it should be displayed at 400px wide with CSS, the result is an image
rendered at double the pixel density:
{% Codepen { user: 'web-dot-dev', id: 'QWBKrjX', height: 700, theme: dark, tab: 'css,result' } %}
A single source image, cut to suit the largest possible space in your layout and high-density displays, works for all users visually, of course. A huge, high resolution image source rendered on a small, low density display will look like any other small, low density image, but feel far slower. The user will be left bearing all the performance costs of that massive, 4000px-wide image source, to no benefit.
For a long time, <img> largely did one thing— it "got image data and put it on the screen." It did that reasonably well, for certain,
but <img> wasn't up to the task of accommodating the radical shifts in browsing context we were experiencing. While responsive web design
became a mainstream development practice, browsers optimized the performance of img for nearly twenty years—but for all but the most privileged
users, the image content of pages was inefficient from the outset. No matter how quickly the browser managed to request, parse, and render
an image source, that asset would likely be far bigger than the user needed.
Image formats: JPEG
JPEG is the most common type of image used on the web, and with good reason: for decades, JPEG has almost invariably been the right choice for encoding photographs. The use case is right there in the name: JPEG stands for "Joint Photographic Experts Group," the committee responsible for first issuing the standard in 1992. You'll see the file extension for a JPEG as either .jpg or .jpeg, though the latter is rare in modern usage.
While lossless compression passively compresses image data as best it can, JPEG's lossy compression looks for opportunities to make the compression more efficient with tiny, often imperceptible alterations to the image data. JPEG encodes image data as eight-by-eight blocks of pixels and describes the blocks—not the individual pixels inside them—algorithmically.
It may sound like an academic distinction on paper: "GIF uses a grid made up of pixels versus JPEG uses a grid made up of smaller grids of pixels." In practice, this use of blocks rather than pixels means JPEG is well suited to a much more common use case for images: the kind of subtle, layered gradients that make up a real world photograph.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/TFKgf2FgDGkoyg7wnVai.png", alt="Horizontal alignment of green blocks going from light to dark.", width="800", height="67" %}
Describing even a very simple single-pixel gradient using GIF-style encoding would be extremely verbose:
Row one, columns one through nine are #00CC00. Row one, column ten is #00BB00. Row one, column eleven is #00AA00. Row one, column twelve is #009900. Row one, column thirteen is #008800. Row one, column fourteen is #007700. Row one, column fifteen is #006600. Row one, column sixteen is #005500.
Describing a gradient using JPEG-style encoding is much, much more efficient:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/ttnejYz6sQKRpzv1v8cl.png", alt="An eight-by-sixteen grid of green blocks ranging in hue from light to dark.", width="800", height="402" %}
Block one is #00CC00. Block two is a gradient from #00CC00 to #005500.
Where JPEG really shines is in quantizing the level of "high frequency" detail in an image, often imperceptibly. As a result, saving an image as a JPEG usually means reducing the quality of that image in ways that are measurable, but not necessarily visible. As you've learned, GIF-style palette quantization is a relatively simple operation: reducing the number of colors in an image results in smaller files, but in a way that can be easy to spot with the human eye.
JPEG, on the other hand, is clever about how quantization is performed: JPEG's lossy compression attempts to quantize an image source in a way that loosely matches the way our own psycho-visual systems quantize the world around us. In effect, JPEG tries to throw away details we weren't likely to see in the first place, so it can sneak additional compression past us.
The human psycho-visual system does a great deal to "compress" the images you're constantly taking in. When I glance outside at my tiny garden, I can immediately process a huge amount of information: for example, individual brightly-colored flowers stand out. I register right away that the soil is a dusty gray, and the leaves are drooping—my plants need water. What I do see, but don't fully process, are the exact shape, size, angle, and shade of green for any of the individual dropping leaves. I can actively look for that level of detail of course, but it would simply be too much information to take in passively, and to no real benefit. So my psycho-visual system does a little quantization of its own, distilling that information to "the leaves are drooping."
In effect, JPEG works the same way. JPEG's lossy compression reduces the level of detail in an image in a way that might not register at all to our naturally "lossy" psycho-visual systems, if done within reason—and introduces far more opportunities for bandwidth savings than lossless compression alone.
For example, JPEG takes advantage of one of our major psycho-visual weaknesses: our eyes are more sensitive to differences in brightness than differences in hue. Prior to applying any compression, JPEG uses a process called "discrete cosine transformation" to split the image into separate frequencies—"layers," in a manner of speaking—representing luminance (brightness) and chrominance (color), or "luma" and "chroma."
The luma layer is minimally compressed, discarding only small details that aren't likely noticeable to the human eye.
The chroma layers are reduced significantly. Rather than simply quantizing the palette of the chroma layers like GIF, JPEG can perform a process called "subsampling" wherein a chroma layer is stored at a lower resolution. When recombined by effectively stretching the lower resolution chroma layers over the luma layer, the difference is often imperceptible. Slight differences in the hue might be noticeable if we compare the original image source and our JPEG side-by-side, but only when we know exactly what to look for.
With that said, JPEG isn't perfect. As crafty as JPEG is about sneaking compression past us, it can become glaringly obvious if taken too far. In fact, if you've spent much time on the web—particularly in days long past—you may have noticed the result of JPEG compression taken too far:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/ar5QsCECxAJLdAHFlwNd.png", alt="A highly compressed image of flowers containing many visual artifacts.", width="800", height="637" %}
Compressing image data too aggressively means the level of detail is reduced further than our psycho-visual systems would naturally ignore, so the illusion is broken. It becomes obvious that detail is missing. Because JPEG operates in terms of blocks, the seams between those blocks can start to show.
Progressive JPEG
Progressive JPEG (PJPEG) effectively reorders the process of rendering a JPEG. "Baseline" JPEGs are rendered from top to bottom as the transfer progresses, while progressive JPEG breaks rendering into a set of full-sized "scans"—likewise done top-to-bottom—with each scan increasing the quality of the image. The entire image appears immediately, albeit blurry, and grows clearer as the transfer continues.
It seems like a strictly technical difference on paper, but there's a huge perceptual benefit: by delivering a full-size version of the image right away instead of empty space, PJPEG can feel faster than a baseline JPEG to the end user. In addition, except for the smallest images, encoding an image as PJPEG almost always means a smaller file size compared to a baseline JPEG—not by much, but every byte helps.
There is one tiny trade-off, however: decoding PJPEG is more complex on the client side, which means putting a little more strain on the browser—and a device's hardware—during rendering. That rendering overhead is difficult to quantify in exact terms, but it's very minor and unlikely to be noticeable except for with severely underpowered devices. It's a trade-off worth making and, all told, progressive is a sensible default approach whenever encoding an image as JPEG.
Using JPEG
You might feel a little overwhelmed by all this information. There's good news for your day-to-day work, though: the more technical specifics of JPEG compression are abstracted away, instead exposed as a single "quality" setting: an integer from 0 to 100. 0 provides the smallest possible file size, and as you might expect, the worst possible visual quality. As you progress from 0 to 100 both the quality and the file size increase. This setting is subjective, of course—not every tool will interpret a value of "75" the same way, and perceptual quality always varies according to the content of the image.
To understand how this compression setting works, let's use a common web-based tool for optimizing image files: Squoosh.
Squoosh—maintained by the Chrome team—provides a side-by-side comparison between different methods of encoding and configuring image outputs, with configuration options ranging from a 0-100 global "quality" slider, to the ability to fine-tune details of chrominance vs. luminance resampling. The lower the "quality" number, the higher the compression, and the smaller the resulting file will be.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/17U3oSBnmzOXCvFPu0dV.png", alt="The Squoosh settings panel, with the quality slider highlighted.", width="800", height="783" %}
Here, with the "quality" set to 60, the file size is reduced by 79%. Don't be fooled by the implications of this label: differences in quality across most of the scale will be imperceptible to the human eye, even when performing a side-by-side comparison.
To convert your source image to a progressive JPEG, tick the box labeled "progressive rendering" under "advanced options." Some of these configuration options are almost certainly a deeper dive than the average web project will require, but they can provide you with a closer look at how the things you've learned about JPEG encoding impact file size and quality in real world use cases.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/2jknUAL72eJKef8m2tPP.png", alt="The Squoosh settings panel, with the progressive rendering setting highlighted.", width="800", height="781" %}
There is not much to it in practical terms, despite how complicated we now know JPEG to be internally. Squoosh is just one of countless methods of saving optimized JPEGs, all of which use a similar method of determining the compression level: a single integer between 0 and 100.
Determining the ideal level of compression for your images is a finesse game, given that all the complexity of JPEG encoding is being distilled down to a single integer, especially when so much hinges on the content of an image. Like thinking about blinking or breathing, considering JPEG compression settings changes how we perceive them.
Looking back outside now, after writing all this, my psycho-visual processing has become more manual—now I'm looking at the individual leaves, even though the picture itself—so to speak—hasn't changed. That makes configuring JPEG compression a little fraught: you know exactly what kinds of visual defects to look for. By thinking about image compression in the first place, you're focusing on individual leaves—we're primed to spot slight blurring and the occasional compression artifact. The harder you look for artifacts while fine-tuning your JPEG compression settings the more likely you are to find them, when a user likely wouldn't. Even if that user knew exactly what to look for, they're not likely to visit a page with JPEG compression on their mind.
For that reason, avoid the instinct to upscale an image looking for artifacts—or even look too closely at the parts of our images where you know artifacts will appear first. For users, faint artifacts blend in with the tiny details that their lossy psycho-visual systems usually gloss over. In fact, even with an understanding of the tricks JPEG tries to play, you wouldn't notice those same artifacts while browsing the web unless you went looking for them. For that reason, it's almost always a safe bet to nudge JPEG compression just a little lower than you think might be noticeable—especially when that image is going to be displayed smaller than its intrinsic size due to direct styling, or constraints set by the surrounding layout.
Key performance issues
As it stands now, images are the web's biggest assets in terms of both total transfer size and number of requests per page. The median webpage's total transfer size is roughly 2MB, as of June 2022, with images alone accounting for nearly half of that. It's no exaggeration to say that optimizing image requests may be the single biggest performance optimization you can make.
Later, you'll learn how responsive images have evolved to help with the issues created by trying to serve one image for all eventualities. In this section, discover key performance metrics that relate to images, and how to improve them.
Deferring image requests
While you're about to learn a number of ways to ensure your image requests are as small and efficient as possible, the fastest image request
will always be the one that never gets made. So, right up front, I want to share what may be the most impactful change you can make to the way
you deliver image assets to your users: the loading="lazy" attribute.
<img src="image.jpg" loading="lazy" alt="…">
This attribute ensures that requests for images aren't made until they fall close to the user's viewport, deferring them from the initial page load—the time when the browser is at its busiest—and removing those requests from the critical rendering path.
Simple as it may be in practice, using this attribute can have a huge positive impact on performance: an image that never falls within the user's viewport will never be requested, and no bandwidth will be wasted on images that the user will never see.
There's a catch, however: deferring those requests means not taking advantage of browsers' hyper-optimized processes for requesting
images as early as possible. If loading="lazy" is used on img elements toward the top of the layout—and thus more likely
to be in the user's viewport when the page is first loaded—these images can feel significantly slower to the end user.
Fetch Priority
The loading attribute is an example of a larger web standards effort to give developers more power over how web browsers
prioritize requests.
You're likely aware of browsers' basic approaches to fetch priority:
for example, a request for an external CSS file in the <head> of a document is considered essential enough to block render, while a request for an
external JavaScript file just above </body> will be deferred until render is complete. If the value of a loading attribute on an <img> is 'lazy',
the associated image request will be deferred until the browser determines that it will be shown to a user. Otherwise, that image will have the same
priority as any other image on the page.
The fetchpriority attribute is intended to give
developers finer-grained control over the priority of assets, allowing you to flag resources
as 'high' and 'low' priority relative to resources of the same type. The use cases for fetchpriority are similar to the loading
attribute, though much more broad. For example, you might use fetchpriority="low" on an image only revealed following user interaction
(whether that image falls within the user's viewport or not) in order to prioritize visible images elsewhere on the page, or fetchpriority="high"
to prioritize an image you know will be immediately visible in the viewport as soon as the page is rendered.
Note that fetchpriority differs from loading in that it doesn't fundamentally change browser behavior. It doesn't instruct the browser
to load certain assets before others, instead giving it vital context for the decisions it makes around requesting assets.
Measuring the impact of images
When optimizing image assets, perceived performance is often more important, and more difficult to measure, than total transfer size alone.
Web Vitals provide measurable, actionable metrics and guidance for improving users' experience of the web, highlighting problems such as slow response time from a web server, rendering issues, and interactivity delays. Core Web Vitals are a subset of these goals, laser-focused on the user's direct experience of an individual page—a set of technical measurements that, together, determine how fast an experience feels to a user.
Cumulative Layout Shift
Cumulative Layout Shift (CLS) is a measure of visual stability. It is a metric for capturing how much the layout of the content on a page shifts as assets are loaded and the page is rendered. Anyone that has spent a significant amount of time using the web has lost their place in a long run of text due to a page "jumping" as a delayed webfont or image source is suddenly rendered—or had an interactive element suddenly moved away from your pointer. A high CLS is a nuisance at best, and cause of user error at worst—a "cancel" button shifting to a space previously occupied by a "confirm" button just as the user clicks, for example.
With high load times and the amount of space they can occupy in a layout, it stands to reason that images are a common cause of high CLS scores.
{% Codepen { user: 'web-dot-dev', id: 'NWBbxqo', height: 500, theme: dark, tab: 'css,result' } %}
Thanks to relatively recent changes in modern browsers, it's easier than you might think to avoid high CLS scores due to images.
If you've been working on the frontend for a few years now, you'll be familiar with the width and height attributes on <img>:
prior to the widespread adoption of CSS, these were the only way to control the size of an image.
<img src="image.jpg" height="200" width="400" alt="…">
These attributes fell out of use in an effort to keep our styling concerns separate from our markup, especially as responsive
web design made it necessary to specify percentage-based sizing via CSS. In the early days of responsive web design, "remove
the unused width and height attributes" was common advice, as the values we specified in our CSS—max-width: 100% and
height: auto—would override them.
<img src="image.jpg" alt="…">
img {
max-width: 100%;
height: auto;
}
Having removed the height and width attributes as in the previous example, the only method the browser has for determining
the height of the image in this situation is to request the source, parse it, and render it at its intrinsic aspect ratio, based on the
width of the space it occupies in the layout once stylesheets have been applied. Much of this process takes place after the page has been
rendered, with the newly calculated height causing additional layout shifts.
Starting in 2019, browser behavior was updated to handle the width and height attributes differently. Rather than using the values of these
attributes to determine the fixed, pixel-based size of an img element in the layout, these attributes can be thought to represent
the aspect ratio of the image, though the syntax is the same. Modern browsers will divide these values against each other in order to
determine an img element's intrinsic aspect ratio prior to the page being rendered, allowing it to reserve the space the image will occupy as the layout is rendered.
As a rule, you should always use height and width attributes on <img>, with values matching the intrinsic size of your image source—so
long as you make sure that you've specified height: auto alongside max-width: 100% to override the height from the HTML attribute.
<img src="image.jpg" height="200" width="400" alt="…">
img {
max-width: 100%;
height: auto;
}
By using the width and height attributes on your <img> elements, you'll avoid a high CLS score due to images.
{% Codepen { user: 'web-dot-dev', id: 'YzjpwyE', height: 300, theme: dark, tab: 'html,result' } %}
It's important to note that there's no downside to this approach, as it leans on long-established browser behavior—any browser
with support for basic CSS will work the way it always has, with the height and width attributes in your markup overridden by your styles.
While width and height attributes deftly avoid CLS issues by reserving the necessary layout space for your images, presenting
users with an empty gap or low-quality placeholder while
they wait for an image to transfer and render isn't ideal either. While there are things you can do to mitigate the measurable and perceptible
impact of slow-to-load images, the only way to present a fully-rendered image to a user more quickly is by reducing its transfer size.
Largest Contentful Paint
Largest Contentful Paint (LCP) measures the time it takes to render the largest “contentful” element visible in the user's viewport—the content element that occupies the largest percentage of the visible page. It may seem like an overly specific metric on the surface, but that element serves as a practical proxy for the point where the majority of the page has been rendered, from the user's perspective. LCP is a vital measure of (perceived) performance.
Metrics like DOMContentLoaded or the window.onload event can be useful for determining when the process of loading the current page
has technically completed, but they don't necessarily correspond to a user's experience of the page. A slight delay in rendering an element
outside the user's viewport would be factored into either of these metrics, but would likely go completely undetected by a real-world user.
A long LCP means the user's first impression of a page—the most important content inside the current viewport—is that the page is slow,
or broken outright.
The user perception captured by LCP has a direct impact on user experience. An experiment done by Vodafone just last year found that a 31% improvement in LCP not only led to 8% more sales—a strong result on its own—but of their total number of users, found a 15% improvement in the number of visitors who became prospective customers ("visitor-to-lead rate") and a 11% improvement in the number of users who visited their cart ("cart to visit rate").
On more than 70% of webpages, the largest element in the initial
viewport involves an image, either as a stand-alone <img> element or an element with a background image. In other words,
70% of pages' LCP scores are based on image performance. It doesn't take much imagination to see why: big, attention-grabbing
images and logos are very likely to be found "above the fold."
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/KrYcABGs7oYi9oHHZPJW.png", alt="LCP highlighted in the console of a web.dev page", width="800", height="407" %}
There are a few steps you can take to avoid LCP delays: first, never specify loading="lazy" on an "above the fold" image,
as delaying the request until after the page has been rendered will likely have a massive negative impact on your LCP score.
Second, using fetchpriority="high" can inform the browser that the transfer of this image should be prioritized above images elsewhere on the page.
With those rules squarely in mind, the most important thing you can do to improve a page's LCP score is reduce the amount of time it takes to transfer and render those images. In order to do that, you'll need to keep your image sources as small and efficient as possible (without sacrificing their quality, of course) and ensure that users are only getting the image assets that make the most sense for their browsing contexts.
Conclusion
Image assets are the biggest drain on your users' bandwidth—bandwidth taken away from transferring every other asset necessary to render a page. Images introduce significant issues in terms of perceived performance, both during and after the surrounding page layout has been rendered. In short: image assets do damage.
Daunting as that may be, while "the web would be better off with fewer images" would certainly be true in terms of performance alone, it would also do its users a tremendous disservice. Images are a vital part of the web, and you shouldn't compromise on the quality of meaningful content for the sake of performance alone.
In the rest of this course, you'll learn about the technologies that power our image assets and techniques for mitigating their performance impacts, without compromising on quality.
Image formats: PNG
There are a few things that set PNG (Portable Network Graphics) apart from other formats, though as it was intended to be a replacement for GIF (due to patent disputes long, long ago) it is similar to GIF in several ways. PNG also uses lossless compression, meaning that the image data will be compressed without any loss of visual fidelity. An image's color palette may be quantized—an “indexed color”, with PNG making use of a palette limited to 256 colors, just like GIF. A far more common “truecolor” PNG can contain many, many more colors—up to 16 million.
PNG and GIF both support transparency, though with a major difference. GIF treats transparency as a binary proposition—a pixel is either an opaque color, or fully transparent. PNG supports “alpha channel” transparency, meaning that each pixel can be set to a level of transparency between 0 (fully transparent) and 255 (fully opaque).
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/PKQ634IdtXzy63TdlfpC.png", alt="Two pink flowers showing two levels of transparency.", width="800", height="399" %}
In practical terms, the lack of substantial quantization and lossless compression mean that saving an image as a PNG will never result in a drop in visual quality. However, this almost invariably results in excessively large file sizes compared to more modern web-friendly encodings. The transfer size of a PNG means they are almost never the right choice for photographic content.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/asVXRCrgha5jMqCXwmn6.png", alt="Comparison of JPEG and PNG.", width="800", height="446" %}
PNG was more commonplace in years past largely for a single use case, as the only raster encoding that supported semi-transparency. Today, PNG should only be considered for simple artwork that requires semi-transparency—a company logo containing a drop shadow, for example—and should be compared carefully to the more modern formats that support semi-transparency, such as WebP.
Much like GIF, PNG was designed to solve use cases that are frequently better served by SVG in terms of both scalability and file size. For that reason, you'll sometimes see PNG used as the fallback version of UI elements in the vanishingly small number of browsers that don't support SVG, though these are increasingly rare.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/GhNCZEadcuq93fPfuiQJ.png", alt="PNG and SVG comparison.", width="800", height="598" %}
In practical terms, PNG is a sound choice for maintaining a manageable-sized “canonical” version of a source image, saved in your local development environment or committed to a project repository in case future versions of that image need to be edited or re-saved in alternate formats.
It is worth noting, however, that even though encodings are standardized, different editing tools have different methods of performing that encoding—some much more efficient than others. Before transferring a PNG in any context, be sure to run your files through a tool like Squoosh or ImageOptim.
Prescriptive syntaxes
The <picture> element doesn't render anything on its own, but instead acts as a decision engine for an inner <img> element,
telling it what to render. <picture> follows a precedent already set by the <audio> and <video> elements: a wrapper element
that contains individual <source> elements.
<picture>
<source …>
<source …>
<img …>
</picture …>
That inner <img> also provides you with a reliable fallback pattern for older browsers without support for responsive images:
if the <picture> element isn't recognized by the user's browser, it's ignored. The <source> elements are then discarded as well,
since the browser either won't recognize them at all, or won't have meaningful context for them without a <video> or <audio> parent.
The inner <img> element will be recognized by any browser, though—and the source specified in its src will be rendered as expected.
"Art directed" images with <picture>
Making changes to the content or aspect ratio of an image based on the size of the image in the page is typically referred to as "art directed"
responsive images. srcset and sizes are designed to work invisibly, seamlessly swapping out sources as the user's browser dictates.
There are times, however, where you want to alter sources across breakpoints to better highlight the content, the same way you adapt page layouts.
For example: a full-width header image with a small central focus may work well on a large viewport:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/HWZtREfRzpcy7tvolvkn.png", alt="A header width image of a periwinkle flower surrounded by leaves and stems, being visited by a honeybee.", width="800", height="286" %}
But when scaled down to suit small viewports, the central focus of the image might be lost:
The subject of these image sources are the same, but in order to better focus on that subject visually, you'll want the proportions of the image source to change across breakpoints. For example, a tighter zoom on the center of the image, and some of the detail at the edges cropped out:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/q9IrCVku2h0B4YfHzI3h.png", alt="A zoomed in crop of the periwinkle flower.", width="400", height="315" %}
That sort of "cropping" can be achieved through CSS, but would leave a user requesting all the data that makes up that image, even though they might never end up seeing it.
Each source element has attributes defining the conditions for the selection of that source: media, which accepts a
media query, and type, which accepts a media type (previously known as "MIME type"). The first <source> in the source
order to match the user's current browsing context is selected, and the contents of the srcset attribute on that source
will be used to determine the right candidates for that context. In this example, the first source with a media attribute
that matches user's viewport size will be the one selected:
<picture>
<source media="(min-width: 1200px)" srcset="wide-crop.jpg">
<img src="close-crop.jpg" alt="…">
</picture>
{% Codepen { user: 'web-dot-dev', id: 'poZNxyN', height: 300, theme: dark, tab: 'html,css,result' } %}
You should always specify the inner img last in the order—if none of the source elements match their media or type
criteria, the image will act as a "default" source. If you're using min-width media queries, you want to have the largest
sources first, as seen in the preceding code. When using max-width media queries, you should put the smallest source first.
<picture>
<source media="(max-width: 400px)" srcset="mid-bp.jpg">
<source media="(max-width: 800px)" srcset="high-bp.jpg">
<img src="highest-bp.jpg" alt="…">
</picture>
When a source is chosen based on the criteria you've specified, the srcset attribute on source is passed along to the
<img> as though it were defined on <img> itself—meaning you're free to use sizes to optimize art directed image
sources as well.
<picture>
<source media="(min-width: 800px)" srcset="high-bp-1600.jpg 1600w, high-bp-1000.jpg 1000w">
<source srcset="lower-bp-1200.jpg 1200w, lower-bp-800.jpg 800w">
<img src="fallback.jpg" alt="…" sizes="calc(100vw - 2em)">
</picture>
Of course, an image with proportions that can vary depending on the selected <source> element raises a performance issue:
<img> only supports a single width and height attribute, but omitting those attributes can lead to a measurably worse user experience.
In order to account for this, a relatively recent—but
well supported—addition to the HTML
specification allows for use of height and width attributes on <source> elements. These work to reduce layout shifts just as well
as they do on <img>, with the appropriate space reserved in your layout for whatever <source> element is selected.
<picture>
<source
media="(min-width: 800px)"
srcset="high-bp-1600.jpg 1600w, high-bp-1000.jpg 1000w"
width="1600"
height="800">
<img src="fallback.jpg"
srcset="lower-bp-1200.jpg 1200w, lower-bp-800.jpg 800w"
sizes="calc(100vw - 2em)"
width="1200"
height="750"
alt="…">
</picture>
It's important to note that art direction can be used for more than decisions based on viewport-size—and it should, given that
the majority of those cases can be more efficiently handled with srcset/sizes. For example, selecting an image source better
suited to the color scheme dictated by a user's preference:
<picture>
<source media="(prefers-color-scheme: dark)" srcset="hero-dark.jpg">
<img srcset="hero-light.jpg">
</picture>
{% Codepen { user: 'web-dot-dev', id: 'MWBbPJm', height: 500, theme: dark, tab: 'html,result' } %}
The type attribute
The type attribute allows you to use the <picture> element's single-request decision engine to only serve image formats
to browsers that support them.
As you learned in Image Formats and Compression, an encoding that the browser can't parse won't even be recognizable as image data.
Before the introduction of the <picture> element, the most viable front-end solutions for serving new image formats required
the browser to request and attempt to parse an image file before determining whether to throw it away and load a fallback. A
common example was a script along these lines:
<img src="image.webp"
data-fallback="image.jpg"
onerror="this.src=this.getAttribute('data-fallback'); this.onerror=null;"
alt="...">
With this pattern, a request for image.webp would still be made in every browser—meaning a wasted transfer for browsers
without support for WebP. Browsers that couldn't then parse the WebP encoding would throw an onerror event, and swap
the data-fallback value into src. It was a wasteful solution, but again, approaches like this one were the only option
available on the front-end. Remember that the browser begins making requests for images before any custom scripting has a
chance to run—or even be parsed—so we couldn't preempt this process.
The <picture> element is explicitly designed to avoid those redundant requests. While there's still no way for a browser
to recognize a format it doesn't support without requesting it, the type attribute warns the browser about the source
encodings up-front, so it can decide whether or not to make a request.
In the type attribute, you provide the Media Type (formerly MIME type)
of the image source specified in the srcset attribute of each <source>. This provides the browser with all the information it
needs to immediately determine whether the image candidate provided by that source can be decoded without making any external
requests—if the media type isn't recognized, the <source> and all its candidates are disregarded, and the browser moves on.
<picture>
<source type="image/webp" srcset="pic.webp">
<img src="pic.jpg" alt="...">
</picture>
Here, any browser that supports WebP encoding will recognize the image/webp Media Type specified in the type attribute
of the <source> element, select that <source>, and—since we've only provided a single candidate in srcset—instruct the inner
<img> to request, transfer, and render pic.webp. Any browser without support for WebP will disregard the source, and
absent any instructions to the contrary, the <img> will render the contents of src as it has done since 1992.
You don't need to specify a second <source> element with type="image/jpeg" here, of course—you can assume universal support for JPEG.
Regardless of the user's browsing context, all of this is achieved with a single file transfer, and no bandwidth wasted on
image sources that can't be rendered. This is forward-thinking, as well: as newer and more efficient file formats will come
with Media Types of their own, and we'll be able to take advantage of them thanks to picture—with no JavaScript, no serverside
dependencies, and all the speed of <img>.
The future of responsive images
All of the markup patterns discussed here were a heavy lift in terms of standardization: changing the functionality of
something as established and central to the web as <img> was no small feat, and the suite of problems those changes aimed to
solve were extensive to say the least. If you've caught yourself thinking that there's a lot of room for improvement with these
markup patterns, you're absolutely right. From the outset, these standards were intended to provide a baseline for future
technologies to build on.
All of these solutions have necessarily depended on markup, so as to be included in the initial payload from the server,
and arrive in time for the browser to request image sources—a limitation that led to the admittedly unwieldy sizes attribute.
However, since these features were introduced to the web platform, a native method of deferring image requests was introduced.
<img> elements with the loading="lazy" attribute aren't requested until the layout of the page is known, in order to defer
requests for images outside of the user's initial viewport until later on in the process of rendering the page, potentially avoiding
unnecessary requests. Because the browser fully understands the page layout at the time these requests are made, a
sizes="auto" attribute has been proposed as an addition to the HTML specification
to avoid the chore of manually-written sizes attributes in these cases.
There are also additions to the <picture> element on the horizon as well, to match some exceptionally exciting changes
to the way we style out page layouts. While viewport information is a sound basis for high-level layout decisions, it
prevents us from taking a fully component-level approach to development—meaning, a component that can be dropped into
any part of a page layout, with styles that respond to the space that the component itself occupies. This concern led
to the creation of container queries: a method of styling elements
based on the size of their parent container, rather than the viewport alone.
While the container query syntax has only just stabilized—and browser support is very limited,
at the time of writing—the addition of the browser technologies that enable it will provide the <picture> element with a
means of doing the same thing: a potential container attribute that allows for <source> selection criteria based on the
space the <picture> element's <img> occupies, rather than based on the size of the viewport.
If that sounds a little vague, well, there's a good reason: these web standards discussions are ongoing, but far from settled—you can't use them just yet.
While responsive image markup promises to only get easier to work with over time, like any web technology, there are a number of services, technologies, and frameworks to help ease the burden of hand-writing this markup available. In the next module, we'll look at how to integrate everything we've learned about image formats, compression, and responsive images into a modern development workflow.
Raster images
Raster images can be thought of as a set of pixel-by-pixel instructions for rendering a two-dimensional grid. Common raster image formats include GIF (.gif), JPEG (.jpg), PNG (.png), and WebP (.webp). The way each image format compresses and encodes these instructions differs, resulting in a huge variance between file sizes: a photographic image encoded as a JPEG might only be a few hundred kilobytes, while the same image encoded as a PNG might be several megabytes, without any discernible difference in quality to the end user.
A raster image source scaled beyond its inherent dimensions will appear distorted, blocky, or blurred:
{% Codepen { user: 'web-dot-dev', id: 'ZEjBQqB', height: 500, theme: dark, tab: 'css,result' } %}
For artwork containing real world levels of detail, raster images are the right tool for the job.
Just like choosing between raster and vector images, choosing the appropriate type of raster image ultimately comes down to the use case. When we break raster images down into their encodings, what we're really talking about are the methods used to describe their contents, and the compression methods (or lack thereof) we're applying. Remember that a server doesn't send an image over the wire to a browser, but a stream of bytes describing the pixel grid that makes up that image for the client to recompose.
So, to better visualize the process of encoding a grid of pixels as bytestream data, I want you to imagine that you're acting as the web browser. You have a sheet of millimeter graph paper and a specific pack of brand name crayons. I, as the web server, have the exact same things—but I've already used my crayons to fill the graph paper in with a source image. If I were to send you a plain text message, I couldn't send you the image itself, but I could convey information about an image source in a language we both understand using our shared standard for our “pixel” grid and colors:
Start at the top left. Row one, column one is blue. Row one, column two is blue. Row one, column three is blue. Row one, column four is red.
Using this textual information, you'd be able to perfectly recreate the image I have on my sheet of graph paper.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/pwUWoFFo7MVq58e05NnK.png", alt="Three horizontal blue boxes followed by one red box.", width="652", height="204" %}
Differences in image formats and the way they're encoded as data can be loosely thought of as the way this information has been formatted. For example, the information I've sent you could be just as readily expressed as:
Start at the top left. Row one, columns one through three are blue. Row one, column four is red.
Either one of these descriptions will result in the same image, but the second one manages to describe the same image with fewer characters. This is a lossless method of compressing image data: all the same information—and thus, no reduction of visual fidelity—but fewer bytes transferred over the wire from me to you—from the server to the rendering engine. This is the plain language equivalent of “run-length encoding” for image data, where data is encoded as the value to be repeated and a count, rather than repeating the full value multiple times.
The inverse, lossy compression, might sound like a non-starter at face value—why would you ever want your images to look worse? That isn't strictly the case, though, and it's worth keeping in mind that our eyes don't have perfect fidelity either. Choosing the correct format and settings for image compression is an exercise in finding the balance between the level of visual detail we're able to perceive and the amount of data sent to the browser. Both of these factors are determined by the content of our source image.
The raster image formats are those you are likely most familiar with as a developer—GIF, JPEG, PNG, WebP, and more. You'll learn about the capabilities of each in the next few modules.
Responsive images
Responsive image markup can be divided into two scenarios: situations where the goal is the most efficient possible images, and situations where you need explicit control over what image source the browser selects. You can think of these as descriptive and prescriptive syntaxes, respectively.
In this module you'll learn both approaches. First we'll cover srcset and sizes, and how to help the browser make the best
choice based on what it knows about the user, their device, and their browsing situation. You'll then discover the <picture> element,
which allows for explicit control over source selection based on factors like viewport size and browser support for image formats.
Descriptive syntaxes
A descriptive syntax provides the browser with information about image sources and how they'll be used, but ultimately leaves the decision-making to the browser. This is by far the more common scenario; for the vast majority of images you won't want granular control over browser behavior. Browsers have far more information at their disposal than web developers do, and can make complex decisions based on this information. You can't predict users' browsing contexts accurately—there's too much about their hardware, preferences, and connection speeds that you can't know. At best, you can make educated guesses, but can't know for certain how each user experiences the web. The central use case for responsive images is strictly goal-oriented, from a developer's perspective: allow browsers to make the most efficient image requests possible, based on the information the browser has at hand.
To enable browsers to make these choices, srcset allows you to provide the browser with a list of potential sources for populating a single
<img>, while sizes allows you to provide the browser with information about how that <img> will be rendered. You'll learn how to use
these in the next module.
Prescriptive syntaxes
With a prescriptive syntax, you tell the browser what to do—what source to select, based on specific criteria you've defined. While this use case isn't as common as "just load the most efficient asset to render this image," it allows us to provide usage instructions to the browser to account for information it doesn't have prior to the transfer of assets, such as the content of the sources or their formats.
While srcset and sizes provide you with a syntax for passing information along to the user's browser and allowing it to make decisions about
image sources, there are times when you'll need explicit control over which source file is presented and when. For example, you might
want to display versions of the same image content with different aspect ratios, based on different design treatments across layout breakpoints,
or make sure only browsers with support for a specific encoding receive specific sources.
In these cases, you want explicit control over which source is shown, and when. These are assurances that srcset and sizes can't give us,
by design. To get that control, we'll need to use the <picture> element.
Vector images
Vector graphics are a method of communicating a series of shapes, coordinates, and paths to their rendering context. They are a set of instructions for how an image should be drawn. When that image is scaled up or down, the set of points and lines that the image represents are redrawn to scale. A smooth curve between two points will be redrawn just as smoothly at any size—similar to the way a CSS-defined border on an HTML element is redrawn as that element is scaled in the viewport.
Scalable Vector Graphics (SVG) is an XML-based markup language developed by the W3C. It is a vector image format designed for the modern web.
{% Codepen { user: 'web-dot-dev', id: 'WNKorKN', height: 500, theme: dark, tab: 'html,result' } %}
Any design software dedicated to editing vector artwork will allow you to export an image as an SVG. But being a standardized, human-readable markup language, SVG can also be created and edited with any text editing software, regardless of the software used to create it, though that quickly becomes unrealistic for images of any real complexity. SVG can be styled with CSS, or contain JavaScript that builds behaviors and interactions into the images themselves.
Even beyond the obvious appeal for designers and developers, SVG is also an incredibly powerful format in terms of the end user experience. The descriptive information contained within an SVG source is often highly compact compared to raster image formats' more prescriptive pixel-grid-based information, in the case of simple shapes—to oversimplify somewhat, the difference between telling the browser "draw a 1px red line between 1x1 and 1x5" and "1x1 is a red pixel. 1x2 is a red pixel. 1x3 is a red pixel. 1x4 is a red pixel. 1x5 is a red pixel." The flip side is that the descriptive nature of SVG requires more interpretation—more "thinking"—from the browser. For this reason, complex SVGs can be more taxing to render. In the same vein, a highly complex image could mean a verbose set of instructions, and a large transfer size.
It may take a little trial and error before you're able to instantly recognize an image source candidate as better served by SVG rather than a conventional raster format. There are a few guidelines, though: interface elements like icons are almost always well served by SVG. Artwork with sharp lines, solid colors, and clearly-defined shapes will likely be a strong candidate for SVG.
SVG is a huge topic: an entire markup language made to coexist alongside HTML, with unique styling options and capabilities. For a more detailed introduction to SVG see the MDN SVG tutorial.
Image formats: WebP
Google originally developed WebP as a lossy image format to supersede JPEG, one that was able to produce files smaller than a comparable-quality image file encoded as JPEG. Later updates to the format introduced the option of lossless compression, PNG-like alpha channel transparency, and GIF-like animation—all of which can be used alongside JPEG-style lossy compression. WebP is an unbelievably versatile format.
WebP's lossy compression algorithm is based on a method that the VP8 video codec uses to compress keyframes in videos. At a high level, it's similar to JPEG encoding: WebP operates in terms of "blocks" rather than individual pixels, and has a similar division between luminance and chrominance. WebP's luma blocks are 16x16, while chroma blocks are 8x8, and those "macroblocks" are further subdivided into 4x4 sub-blocks.
Where WebP differs radically from JPEG are in two features: "block prediction" and "adaptive block quantization."
Block Prediction
Block prediction is the process through which the contents of each chrominance and luminance block are predicted based on the values of their surrounding blocks—specifically the blocks above and to the left of the current block. As you might imagine, the algorithms that do this work are fairly complex, but to put it in plain language: "if there's blue above the current block, and blue to the left of the current block, assume this block is blue."
In truth, both PNG and JPEG also do this sort of prediction to some degree. WebP, however, is unique in that it samples the surrounding blocks' data and then attempts to populate the current block by way of several different "prediction modes", effectively trying to "draw" the missing part of the image. The results provided by each prediction mode are then compared to the real image data, and the closest predictive match is selected.
Even the closest predictive match isn't going to be completely right, of course, so the differences between the predicted and actual values of that block are encoded in the file. When decoding the image, the rendering engine uses the same data to apply the same predictive logic, leading to the same predicted values for each block. The difference between the prediction and the expected image that was encoded in the file is then applied over the predictions—similar to how a Git commit represents a differential patch that gets applied over the local file, rather than a brand new copy of the file.
To illustrate: rather than dig into the complex math involved in the true predictive algorithm, we'll invent a WebP-like encoding with a single prediction mode, and use it to efficiently relay a grid of numbers the way we did with the legacy formats. Our algorithm has a single prediction mode, which we'll call "prediction mode one:" the value of each block is the sum of the values of the blocks above it and to the left of it, starting with 1.
Now, say we're starting with the following real image data:
111151111
122456389
Using our predictive model to determine the contents of a 2x9 grid, we would get the following result:
111111111
123456789
Our data is a good fit for the predictive algorithm we've invented—the predicted data is a close match to our real data. Not a perfect fit, of course—the actual data has several blocks that are different from the predicted data. So, the encoding we send includes not just the prediction method to use, but a diff of any blocks that should differ from their predicted values:
_ _ _ _ +4 _ _ _ _
_ _ -1 _ _ _ -4 _ _
Put in the same kind of plain language as some of the legacy format encodings we've discussed:
2x9 grid using prediction mode one. +4 to 1x5, -1 to 2x3, -4 to 2x7.
The end result is an unbelievably efficient encoded file.
Adaptive block quantization
JPEG compression is a blanket operation, applying the same level of quantization to every block in the image. In an image with a uniform composition, that certainly makes sense—but real-world photographs aren't any more uniform than the world around us. In practice, this means that our JPEG compression settings are determined not by the high frequency details—where JPEG compression excels—but by the parts of our image where compression artifacts are most likely to appear.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/ohpy2TSKU5qkO7bZOzIy.png", alt="A compressed JPEG image of a monarch butterfly", width="800", height="798" %}
As you can see in this exaggerated example, the wings of the monarch in the foreground look relatively sharp—a little grainy when compared with the high-resolution original, but certainly not noticeable without the original to compare with it. Likewise, the detailed inflorescence of the milkweed, and the leaves in the foreground—you and I may see traces of compression artifacts with our trained eyes, but even with the compression dialed up well beyond reasonable levels things in the foreground still look passably crisp. The low-frequency information at the top left of the picture—the blurry green backdrop of leaves—looks terrible. Even an untrained viewer would immediately notice the quality issue—the subtle gradients in the background are rounded down to jagged, solid-color blocks.
In order to avoid this, WebP takes an adaptive approach to quantization: an image is broken into up to four visually similar segments, and compression parameters for those segments are tuned independently. Using the same outsized compression with WebP:
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/gJKurkTf09XGFhaUApP2.png", alt="A compressed WebP image of a monarch butterfly", width="800", height="801" %}
The size of both these image files is about the same. The quality is about the same when we look at the monarch's wings—you can spot a few tiny differences in the end result if you look very, very closely, but no real difference in overall quality. In the WebP, the flowers of the milkweed are just a little sharper—again, likely not enough to be noticeable unless you're comparing the two side-by-side and really looking for differences in quality, the way we are. The background is a different story altogether: it has barely a trace of JPEG's glaringly obvious artifacts. WebP gives us the same file size, but a much higher quality image—give or take a few tiny details that our psychovisual systems wouldn't be able to detect if we weren't comparing the two so closely.
Using WebP
The internals of WebP might be considerably more complex than JPEG encoding, but just as simple for the purpose of our daily work: all the complexity of WebP's encoding is standardized around a single "quality" value—expressed from 0–100, just like JPEG. And once again, that's not to say that you're limited to a single overarching "quality" setting. You can—and should—tinker with all the fine details of WebP encoding, if only to gain a better understanding of how these normally-invisible settings can impact file size and quality.
Google offers a cwebp command line encoder that allows you to convert or compress individual files or entire directories of images:
$ cwebp -q 80 butterfly.jpg -o butterfly.webp
Saving file 'butterfly.webp'
File: butterfly.jpg
Dimension: 1676 x 1418
Output: 208418 bytes Y-U-V-All-PSNR 41.00 43.99 44.95 41.87 dB
(0.70 bpp)
block count: intra4: 7644 (81.80%)
Intra16: 1701 (18.20%)
Skipped: 63 (0.67%)
bytes used: header: 249 (0.1%)
mode-partition: 36885 (17.7%)
Residuals bytes |segment 1|segment 2|segment 3|segment 4| total
macroblocks: | 8%| 22%| 26%| 44%| 9345
quantizer: | 27 | 25 | 21 | 13 |
filter level: | 8 | 6 | 19 | 16 |
And if you're not inclined toward the command line, Squoosh will serve us just as well for encoding WebP. It gives us the option of side-by-side comparisons between different encodings, settings, quality levels, and differences in file size from JPEG encoding.
Learn Privacy
When you acquire a new user, you are granted their trust. It's your organization's responsibility to honor this trust by keeping user data private. As people use your service, they will share some information with your site or system so it can function and serve them best.
As a developer, you have a key role to play! Ensuring that your site or system respects user privacy isn't just a product manager's or legal team's responsibility.
You have the power to directly improve the privacy of the people who use your site:
- By factoring in privacy early, as you're making technical and design decisions. This is called privacy by design.
- By using your technical expertise to equip the people you work with so they can make informed privacy decisions themselves.
Just like making the right security decisions, making good privacy decisions shouldn't be scary! This course is here to help.
What you will learn
In this course, you will learn about:
- Pragmatic privacy techniques. We'll talk about making your site privacy-preserving. To be mindful about all the bits of data that flow through your site, you'll learn a few encryption tips and understand what it means to collect just the data you need. We also know that it may be helpful to collect some user data and to leverage third parties as building blocks for your site to provide useful services—be it payment solutions, map widgets, or error monitoring scripts. This course will offer tools and tips for you to approach this with care, especially when it comes to third parties.
- What's up with web privacy. You will learn about upcoming browser and web platform privacy improvements, and how you can prepare your site for these changes. You'll also learn about what fingerprinting protections in browsers mean for your site.
- Ways to scale and maintain your organization's privacy approaches. In best practices, you will find tips to help the people you work with—direct teammates, members of other teams, or your company's leadership—consider closely the impact of their privacy choices, and make decisions accordingly.
Prerequisites
To follow this course, you need an intermediate knowledge of HTML, JavaScript, and HTTP requests and headers.
Why this course matters today
- User concerns and user experience. People are increasingly concerned about their privacy on the web. Offering more privacy and being mindful about data collection can help build trust in your service and improve their experience, directly contributing to your organization's success.
- Compliance. Over recent years, privacy regulations have emerged all over the world, and keep evolving: in 2017, the rehauled APPI in Japan; in 2018, the GDPR in Europe, in 2020, the CCPA in California; in 2022, the PDPB in India … Whether or not you've directly worked on tasks to comply with these regulations, this shift indicates regulators’ growing attention to privacy.
- Readiness for a more private web. Important privacy improvements have been made and will keep coming to browsers and to the web platform. Browser vendors are limiting cross-site tracking enabled by third-party cookies, and are actively combating fingerprinting and other covert tracking techniques. Safari's ITP, Firefox's ETP, and third-party cookie phase-out plans in Chrome are a few recent examples. Besides browser features, changes to the web platform itself are coming: ecosystem actors, browser vendors, and developers are incubating ideas for new web APIs to replace old ones, and fulfill important use cases in a privacy-preserving way. All these changes make the web a safer and more private place for users, and they may come with a better developer experience too! While these changes are improvements, you'll need to take measures to adapt your site or system to them.
Bear in mind
This course doesn't offer legal advice, and it isn't a checklist. Instead, it lays out code snippets, best practices, and questions to ask yourself and your team on your privacy journey. Throughout this course, we refer to products or documentation maintained by third parties to illustrate key points. Your own approach is up to you to define. It will depend on your organization, your business, and your compliance requirements and users’ needs.
Here's what you'll learn:
{% include 'partials/course-index.njk' %}
Let's get started!
{% Aside %} This course was written by Stuart Langridge, with input and review from Maud Nalpas, Martin Sramek, and Rachel Andrew. Special thanks to David Adrian for providing additional support for the encryption module, Rainhard Findling for providing additional support for the data module, and Annette McFadyen and Aaron Forinton for supporting the publication process. {% endAside %}
Best practices
Over this whole course, certain themes have come up again and again. Protecting your users' privacy involves knowing the minimum that you need about them, being honest and transparent with what you need and why, and removing what you have as soon as you no longer need it. You are also responsible for what others are able to do and are allowed to do with your users' data, and that means that you need to be able to explain honestly and transparently what that is. Any data that you don't need, you should not have; any data that you do need, you should be able to explain why you need it, and for how long.
In larger organizations, there may be roles or teams dedicated to tracking the latest technical changes in your deployment environments and browsers, and to understanding the implications for technical and legal changes on your users' privacy. But a smaller organization still needs to be conscious of user privacy, how a changing environment can affect decisions already made, and what needs to be taken into account for decisions you make from now on. This module summarizes some best practices for how you can stay in touch with your privacy choices and requirements, and those of your users.
Be aware of what you do
The first best practice here is understanding. You need to be aware of what you know about your users, and why you know it; you need to be aware of what your partners know about your users, and how they found it out. This should be a documented part of your privacy policy. It is difficult and time-consuming to put this list together at first, but more importantly it's also eye-opening for you and for the business. It can often come as a surprise (and not a nice surprise) just how much data you collect and store about your users. Documenting it will give you a level of awareness, not just into data collection and privacy, but into the user experience of your systems and into the underlying software as well. There are frequently dusty corners or superseded requests which are in need of updating and which have been forgotten about.
Do
For each piece of data that could be tied to a user, explicitly document:
- A specific list of what it is collected for.
- When it will be deleted (and how it can be deleted by the user, not only by your own team).
- How you collect it.
Ensure that it is gathered only at sufficient granularity to answer these questions alone.
This documentation is for internal use and should be full and complete, but it can be valuable to document this publicly for users, because it is important to establish an atmosphere of trust. This is not only beneficial in general terms for the customer relationship, but your users are more likely to volunteer the data you need to make business decisions if they are confident it will not be misused. This public documentation is linked to your more general privacy policy (in fact, it will form the great part of your privacy policy), and having this written in a form that your users can understand (alongside the legal language) helps to build the trust relationship.
Stay up to date
The second-best practice is to stay up to date. This whole industry moves quickly as a general rule, and privacy is a fast-changing field; staying up to date can itself be a challenge. Technology that's available to you will alter frequently, but user expectations will also alter just as fast. It is important to not fall behind, and if you can stay ahead, then there is a real competitive advantage to be had by staking out a position as privacy-preserving. Managing user privacy and understanding how the industry may be changing may not be all of someone's job, and you do not need to be experts, but it should be some of someone's job. Dedicate some of the training or conference budget to staying in touch with industry trends and regulatory updates.
It can be difficult to keep up with changing attitudes and best practices in privacy. There isn't a convenient single place to go for this. This is partially because privacy protection is a wide-ranging field which impacts on many different parts of many different industries. But the very topic of how best to protect the privacy of your users and others is heavily disputed and there are many different and often conflicting approaches. In this course, we have laid out a path to follow and some best practices, but it will be useful to you to synthesize your own approach as best fits your goals, your organization's , and your users' needs. We have compiled a list of resources available for you, your management, and the team around you to help you stay up to date with changing norms and what currently constitutes the best things to do.
Some privacy changes made by browsers are technical in nature, and need to be understood by dev teams. Consider, for example, the change to SameSite=Lax by default on cookies. This change affected features on some sites, and so may have needed technical changes. It was announced in advance and trialed before being finally rolled out. This is a good example of the sort of change that can affect user privacy (in this case, that change was an improvement) and may also need changes to your apps to correctly deal with.
Resources
Browser vendors and web platform developers
For most web development teams, the best places to stay up to date with the latest industry practices on privacy and user protection are the browser vendors and consumer data protection organizations. For announcements and press releases, this means the various browser vendor team blogs: a good portion of these are about technologies and things unrelated to privacy, but where there are privacy-related announcements, they'll show up there:
- Webkit (https://webkit.org/blog/)
- Chrome (https://developer.chrome.com/blog/ and https://blog.chromium.org/)
- Firefox (https://hacks.mozilla.org/)
- Edge (https://blogs.windows.com/msedgedev/)
- Samsung Developers (https://developer.samsung.com/blog).
To get a sense of what the browsers plan to implement and to work out their stances on upcoming and proposed APIs that you're thinking of using, there are status pages and positions pages:
- WebKit (https://webkit.org/status and https://github.com/WebKit/standards-positions)
- Chrome (https://chromestatus.com/features and https://developer.chrome.com/tags/deprecations/ for things being removed)
- Edge (https://developer.microsoft.com/en-us/microsoft-edge/status/)
- Firefox (https://mozilla.github.io/standards-positions/)
Privacy organizations
Of course, the browser vendors' positions are only one input to this conversation. There are also organizations pushing for improving privacy protection over the current state, and it's worth keeping tabs on them. The list is long, but here are just a few examples:
- The EFF's Privacy issue (https://www.eff.org/issues/privacy).
- The Open Rights Group's digital privacy initiative (https://www.openrightsgroup.org/category/online-privacy).
- IAPP (https://iapp.org/).
Governmental organizations
To keep a closer eye on this whole area, there are also other actors in the field to follow. Governmental organizations' decisions and their approaches have some of the greatest impact:
- European Data Protection Board in the EU.
- ICO in the UK.
- The California Privacy Protection Agency in California.
- Data Protection Africa
- IAPP's overview of data protection across Asia.
Mainstream media privacy reporting can often focus primarily on "big tech" firms and governmental organizations, which may seem removed from work done at a smaller company or organization. It's also often very US-centric. But even so, it's useful to keep aware of what the rules might be in the future, so that you can be prepared, as can the team and the management structures around you.
It's also worth checking out Understanding Privacy, published in November 2022 by Heather Burns, which gives a really good look at the whole field of data privacy and what you need to know about it. Recommended!
Looking inward
Staying up to date also means staying up to date with your own stance. As part of understanding your own software, you will have conducted audits and reviews of your own data collection and your third-party partners. These audits are not a one-time-only thing: they should be repeated at regular intervals and kept updated as software changes. It can be useful to mandate that the privacy audit documentation is kept updated as part of development, just as other technical documentation is. If a new release collects more data, then updating the privacy audit to include what is collected and why and when it will be deleted is of similar importance to a new release's APIs being described in the public documentation.
Do
- Stay up to date with how the industry and user expectations around privacy may be changing over time. It's often the case that someone on a team is particularly interested in privacy-related topics, even if there isn't the budget or sufficient need for a full-time role. Consider formalizing this by making privacy an official part of someone's job, with accompanying benefits for the responsibility.
- Make keeping the privacy audit documentation from "Understanding" earlier, part of your documentation process just like API docs are.
- Re-run the audits of which data you collect and which third parties you use, either on a regular basis or when major features change; test your software as a new user to discover what information is asked for, and add this to the audits.
Prevent overreach and control access
The third best practice is to prevent overreach: that is, to avoid doing more with data than you committed to, and to avoid speculatively gathering data in case it becomes useful in the future. That involves adapting your processes so that you set the culture you want around protecting user privacy. Cultural change is difficult, but once accomplished it will largely maintain itself, which is easier.
Document your uses
Consider one of the best practices discussed earlier, that of documenting what exactly user data is collected for. This documentation is important for you to understand what you do and why, but it is equally important to ensure that this rule is obeyed. If someone suggests using already-gathered data for new analyses, push back on that suggestion, because that's not what the data was gathered for. This is helped by other aspects of data understanding: that data is only gathered at the lowest acceptable granularity, and is deleted after being used, because it's impossible to reuse existing data for new analyses if there is no existing data.
Have processes and rules around handling user data
This can be difficult. It's hard to explain the nature of the user relationship in these situations, when there may be insights to be gained and nothing preventing that other than a promise previously made. But it's important to consider that the user entrusted the data to you for a specific purpose, and you (and your team) shouldn't abuse it for something else. A good approach here is to require some amount of process around access to user data. It is very important to avoid inserting a mandatory "privacy" component into every working process as a substitute for actually caring about the issue, because it can quickly become a "checkbox" feature that everybody ignores (and nobody likes more paperwork, especially if it's paperwork that nobody reads).
Make avoiding overreach be the path of least resistance
But there is a chance to use the annoying nature of bureaucracy slightly for your own benefit here! If mining existing collected data requires filling out a "privacy request" where the new analysis is justified and recorded, then projects which don't really need that access are quite likely to avoid it in order to avoid the bureaucracy, or in order to avoid being named on the paperwork. You may already have policies in place around the security of user data: saved account details are restricted and not available to employees without justification. Consider tying privacy requirements to these existing policies. If there's some consideration of user privacy built into your processes at an early stage, that consideration can quickly become a routine part of planning. It's vital that architects, developers, and marketing do not see privacy protection as an onerous externally imposed restriction, but as a core part of the customer relationship.
Pushing back with alternatives, not stop energy
With the preceding best practices in place, you will have gathered user data for only specific and measurable aims, and your user base will have been informed of those aims and understand them. However, you will also have a large collection of user data, and it's quite common that the business will seek to use that data for reasons other than those for which it was gathered. It's your goal to push back on those uses, but it's important to do so by providing alternatives. Imagine that you've requested that your users provide an age bracket that they fall into: 18-25, 25-35, 35-50, 50+. You did this in order to measure which types of products are most bought by different age brackets, and you explicitly informed users that's why their age was requested. If someone then suggested that they could use that data to send advertising email to every user under 25, that's a new and undeclared use for existing data, and so isn't allowed. But your intention here should be to find a way of meeting the business need without using data for things that weren't declared. If you push back on this without proposing any alternative, then user privacy will start to seem like the externally imposed restriction warned against earlier, rather than a core part of why your users trust you. As much as possible, avoid adding stop energy to processes: the only thing worse than a bald "computer says no" is "internal regulator says no". Instead, consider other ways you could meet that goal without using users' personal data to do it: perhaps use the list of products purchased as a guide for who to email, or avoid targeted distribution entirely. Help your team understand why your users trust you, and what that trust is founded upon, and then help them do what they want and what your users want. Make privacy work for you.
Do
- Require a (short, simple) written justification from a named employee for access to user data.
- Add user privacy requirements as early as possible in your processes.
- Avoid adding privacy concerns as a mandatory "checkbox" feature.
- Enforce the deletion of data as defined earlier.
- Help the rest of the team understand how to achieve their goals without compromising your users' privacy.
Use just the data you need
A good way to mitigate risk for users is to not hold sensitive data about them that you do not need that impacts their privacy. There are a surprising number of ways to do this while still meeting your business goals, and it is worthwhile to consider each. You might:
- Explain what you need the data for.
- Collect data at lower granularity.
- Remove data once used.
- Not collect it in the first place.
Each of these approaches can help your users feel more comfortable with what you're doing and why, and this contributes greatly to your relationship with them. Transparency builds trust, and importantly, trust can be a unique selling point for you. Many people assume that users and customers trust them by default, but consumers are constantly evaluating products and services and this may not be the case. If you build a relationship with your users where they trust you to handle their data and your interactions with respect, it can deliver a competitive advantage to you as a project or a business: it is something that your rivals may not match, a genuine differentiator.
Let's unpack the above approaches, in order of most effective (but also most impactful on your business) to least effective but least disruptive to implement.
Don't collect it in the first place
The most obvious way to avoid compromising your users' data is to not collect it. Some data collection is necessary to provide services, but there are more places where you can avoid data collection than you might think. Consider, for example, guest checkout. When users come to purchase something using your web app, you might require them to sign up for an account, because then you've captured personal details for later fulfillment: they can be added to the mailing list, they're already pre-qualified as an interested customer, and so on. However, customers recognise this, and don't like it: in 2021, a study found that one out of four abandoned sales was because the site demanded that the user create an account. If you don't require an account, you're more likely to keep those customers. Making it possible to complete a purchase without signing up gives users better options and also means you don't have as much of their data to protect and secure.
"Fuzz" your data
Of course, avoiding collecting data at all may not be an option. It is important to collect data to provide services and make sensible business decisions. It can also be helpful to build marketing communications in the context of a trusting relationship. However, it's also important to realize that decisions made in aggregate (that is, which affect many users at once) are made about data in aggregate (that is, about collective properties of the data).
For example, it is sometimes useful to have a sense of the demographics of your audience: which age brackets they fall into, location, and so on. This may change your messaging or your approach. But this does not mean that you need to collect the exact age for every user of your service. What you're often looking for are trends and overall properties. If the decision you wish to reach is affected by whether most of your audience is in the "18-34 key demographic", then the only question you actually need to ask is whether your users are in that demographic. This collects them into two "buckets": in that group and not in that group. There may be situations where you need more granular data than that, but it's entirely reasonable to take the list of demographics you use to make decisions and ask your users to classify themselves with that list.
Example
So, if it is useful to know how your user base divides up between the age categories "18-34", "35-49", "49-64", and "65+", then you can ask your users to choose which of those categories they fall into. It is tempting to ask for extremely granular, personal and personalized data, and then classify your users yourself, as this avoids needing to ask the same question again in more detail later; for example, to ask for an exact age and date of birth, and then use this to produce your own lists of how many users are in the "35-49" category. But it's important to realize how this looks: as the course has already covered and will cover again, asking for detailed levels of data can make people uncomfortable and so reduces user trust in your organization, while adding risk.
It's also important to consider your data needs. Sometimes, the "need" for more granular data is speculative, a "just in case" requirement. Maybe we only need to classify users into those four age groups right now, but in the future we might want to narrow that down, and therefore we should collect very detailed data now to keep that option open for later. It may be worth considering how often the more granular data has actually been used in the past to guide decisions. Asking for data that is perceived as invasive relative to the service being offered necessarily results in a decrease in how much your users trust your organization. If that data is being gathered for “just in case” reasons, then you may not just be trading away user trust for improved business decisions but trading it away merely for the possibility of some theoretical future decision that may not even exist, while also taking on security requirements for that information.
There are more detailed algorithmic ways to reduce the granularity of collected data as well. Randomized response methods mean that the data is gathered with a tuneable degree of inaccuracy, and these have been used for decades in the social sciences when collecting potentially invasive or sensitive data while maintaining the responder's confidentiality. The above method of data gathering involves widening the user's answers (so "how old are you" becomes "which one of the following age groups do you fall into"), where randomized response involves having a certain proportion of users lie about their answers. If the proportion of users who respond incorrectly is known, then meaningful conclusions can still be drawn from the gathered data, but individual user's privacy is not compromised because their collected data may be incorrect. In this case, if 80% of your audience still state that they fall into the 18-34 demographic, you can be relatively confident that this is still the largest share, even if 10% of them are deliberately giving incorrect answers. The degree of incorrectness can also be altered programmatically, where correct answers are always solicited but the software alters a certain percentage of answers before transmitting. This process and the consequences of it can also be explained to users when data is gathered: it means that the users do not have to trust that you will not abuse their collected data, because individual data is unreliable.
A similar but more technically involved process is differential privacy. This uses mathematical techniques to alter data storage so that aggregate properties of the data are still present, but it is not possible to even tell whether a particular individual even provided data, or which if any data they provided. Like randomized response, this protects users' data even from you and demonstrates clear intent on your part: you can't use your users' data if you don't have that data.
These and similar approaches also provide increased security against data breaches and leaks, because the collected data reduces compromises to user privacy, even to you, and will also reduce the level of compromise if the data is leaked. Remember, though, that if you're applying differential privacy techniques on the server (so your users are sending you unaggregated data and then you use the techniques to aggregate it), you still need to secure that raw user data and then delete it after processing, and should have and follow clear policies to confirm that you're not using it before aggregation (or are clear about what you are using it for).
Retention: collect data and then remove it once used
It's useful to remember that collected data has a lifecycle; it's collected, it gets used to help you make business decisions, and then, at some point, it should be removed. These are, again, trade offs: when you ask your users questions, or you store information about other websites they've visited, or you track which things they looked at and for how long in order to make predictions about their preferences, this is the data which is being granted to you for a specific purpose-not as an open-ended grant for the developer to use as they see fit. When that data is no longer needed for that purpose-sometimes after a minute, sometimes after many years-it should be deleted.
Whenever you gather information about your users, you should know what you'll be using that data for (see below) and you should also know when and why you will stop holding that data. This might be when the user chooses to delete it, or when they sign out, after a specific time period, or after a specific event takes place. An excellent way to build trust in the relationship is to make it clear to your users how they can control data about them, including, wherever possible, a unilateral opt-out. How do they delete their data? How do they delete their account? In addition to helping to build that relationship, it is best practice to store data for as long as you need to process it and not longer, and that there should be a way for your users to see and remove data you collect from them or on their behalf. There may even be legislation on this point in territories in which you operate.
This is an area where you can define clear technical goals, which helps users with self-service; if your users can opt out of your data warehouse without having to ask permission then they can feel much more comfortable with opting in, and it doesn't take any support resource to do so.
It's important to recognise the importance of easy and default opt outs: "To build trust and recognition, companies can start by agreeing to a social contract where they commit to respecting their audience at each and every touchpoint, listening to their needs, and responding accordingly", states IAPP. The Nielsen Norman Group says that users "need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process". Everyone is aware that it's easier to subscribe than to unsubscribe. But, as Nielsen Norman says, giving users the ability to walk away without having to jump through hoops, "fosters a sense of freedom and confidence". Academic studies back this up and name it the "principle of revocability", stating: "The interface should allow the user to easily revoke authorities that the user has granted wherever revocation is possible. Users should be able to revoke such consent and therefore reduce authorities to access their resources if possible." (See Yee and Iacono for examples.)
How long to retain data for, and which data to retain, is a subject which differs a great deal between organizations and between projects, but there are some common guidelines to consider.
Do
It is useful here to allow users to delete accounts (and any associated data, where it is possible to do so) and to regularly (for example, on sign out) clear ephemeral and locally stored data on sign-out with the Clear-Site-Data header.
Supply a Clear-Site-Data header to remove some or all user data which has been stored client-side (whether in cookies,
localStorage or IndexedDB, or in the browser cache), when reasonable. The obvious use-case for Clear-Site-Data is when a user
logs out, but it can also be used after security incidents to ensure that a potentially compromised account has no lingering traces
of compromised data stored on the client.
Adding support for Clear-Site-Data involves sending an HTTP header, Clear-Site-Data, when the user signs out (or at other
times when you wish to clear client-side storage), on the page which confirms the logged-out status (https://your-site/logout
or similar). This header can have some or all of the following values, or "*" for all:
Clear-Site-Data: "cache", "cookies", "storage"
These values clear, respectively, cached pages (and other HTTP-cached resources), stored cookies, and localStorage and IndexedDB and similar.
You may see reference to another option, executionContexts, but this is not supported by many browsers.
Note that using the Clear-Site-Data header is likely to be easier than individually deleting all created resources yourself, because it does not require JavaScript code to be
run on the client (and it is the only official way to clear the browser cache), but is not supported by all browsers.
Usage note: If you're clearing the cache (by sending Clear-Site-Data: cache), then the Clear-Site-Data header should not be
sent on your actual sign-out page, but on some other resource that that page loads. This is because on a slower computer
with a large cache, the page will block while it clears the cache and this prevents navigation. This can take minutes to do,
which will frustrate the user. It is unlikely to happen, but is difficult to test for and thus it is best practice to bear this in mind.
Explain what you need the data for
The importance of trust in your users' relationship with your service has been stated repeatedly, because it increases user longevity. It also provides competitive advantage. One way to increase that level of trust is with transparency into your processes, and a good way to be transparent is to explain what you want data for. You learned earlier that for each thing collected you should know when that thing will be deleted. In order to know that, you need to know why you want this data, which specific questions need it in order to find answers, and which decisions will be guided by collecting it. Once you know why you need this data that you've asked your user to give up, it will help to build trust by explaining that to those users. In your privacy policy, or when asking questions on account creation, describe why you need the answer to this particular question, what you'll do with that data, and when and how it can be removed.
These explanations are much more visible when presented inline. Burying explanations in a dense policy document elsewhere on the website can seem like an attempt to hide them. A sign-up, checkout, or request form can present the reasons to collect data alongside the collection itself. Often, a form field may have an asterisk (*) to indicate that a field is required; complicated forms often have an information link (i) to explain what the field means. Consider adding to these explanations a description of why the data is being collected. A frequently used phrase for this is "Why do we need this?" next to a form field, which when clicked shows a pop-up explanation.
Some example HTML might look as follows, and then CSS and JavaScript would take care of hiding the <aside> and showing it as a pop-up when
the link is clicked. (Be sure to confirm the accessibility of the form you make for your site!)
Exactly how to lay this out is dependent on your styles and approaches, but the main point here is to directly associate data collection with
an explanation for why that data is being collected. This isn't necessary for every field. Nobody needs an explanation of why you require them to
choose a password at sign-up. But decorating each request for personal and contact information with how you plan to use it and keep it can help
make it clear to your users that you're invested in protecting their data.
<div>
<label for="email">Email address*</label>
<input id="email" type="email" name="email" required aria-describedby="whyemail">
<a href="#whyemail">Why do we need this?</a>
<aside id="whyemail">We need this information as a unique identifier for you, and if you forget your password we can send you a reminder. We will use your email address to send you regular updates on the service if you choose, and will delete your email address from any mailing lists if you delete your account.</aside>
</div>
Going through this process with everything you collect about a user can also help with internal processes and discussions. Earlier, you saw how there can be a temptation to collect data "just in case". When you are transparent about your reasons for collecting, it can be quite obvious that this is happening. If you are reluctant to write down publicly what you want to do with user data because those users won't like the explanation, this may be a sign that it is worth rethinking collecting it. This applies whether the distasteful explanation is too invasive ("we will use this to track where you visit on an hourly basis"), too wide-ranging ("we don't know what we'll use this for yet but we want it in case we think up something for it"), or too evasive ("we will use this for internal undisclosed purposes"). This is not simply a question of morality; people are savvy enough to recognise this, as already described, and there is a user expectation that experimenting with something is not the beginning of a long-term commitment. It is a commonplace of user experience design to make sign-up as friction-free and easy as possible, because at early stages the user is (by definition) not heavily invested in your service, and so it is important to allow them to become more invested easily when they yet have little inclination to do so. If it is as easy to leave again, then experimenting with the service becomes exactly experimentation and not the unwilling start of a forced long-term commitment. As before, it is paradoxical but true that the best way to build trust is to not require your users to trust you if they do not want to.
People have good reasons for not sharing data, or for sharing minimal data. At the beginning of your relationship with them, they might not have a reason to trust you, and should not have to. Your goal is to demonstrate why they should.
Do
- Decide for all the data you plan to collect why you want it and how long you'll keep it for.
- When you ask for that data, explain to your users why you are collecting it.
- Delete it from your server databases after you've used it.
- Allow users to delete accounts that they have created and clear stored data from their storage with the
Clear-Site-Dataheader.
Why
Building a relationship with your users is about trust, and trust is about openness. If you can demonstrate that you're not just collecting as much data as possible about your users and concealing your uses for it, then that helps to build trust, which can be a competitive advantage for you over less scrupulous rivals.
Encryption
Encryption is often a topic for security, but it's important for privacy too. The goal of encryption is to prevent others from reading the encrypted information… but preventing others from reading your information is one way to keep it private. A user is often limited in how much they can do that themselves, but with your assistance as provider of a service they're using, encryption can help to keep their data theirs.
There are three relevant ways to apply encryption to aid user privacy: encryption in transit, encryption at rest, and encryption end-to-end:
- Encryption in transit is keeping data encrypted between the user and your site: that is, HTTPS. You probably have HTTPS set up for your sites already, but are you sure all the data in transit to your sites is encrypted? This is what redirection and HSTS are for, and they are described below and should be part of your HTTPS setup.
- Encryption at rest is the encryption of data stored on your servers. This protects against data breaches and is an important part of your security stance.
- End-to-end encryption is the encryption of data on the client before it ever reaches your server. This protects user data even from you: you can store your users' data, but you can't read it. This is difficult to implement, and isn't suitable for all types of application, but it's a strong aid to user privacy, because nobody can see their data other than themselves.
HTTPS
The first move is serving your web service over HTTPS. It is very likely that you will have already done this, but if not it is an important step. HTTPS is HTTP, the protocol that a browser uses to request web pages from a server, but encrypted using SSL. This means that an outside attacker can't read or interfere with an HTTPS request in between the sender (your user) and the recipient (you), because it's encrypted and so they can't read it or change it. This is encryption in transit: while data is moving from user to you, or from you to user. HTTPS encryption in transit also prevents the user's ISP, or the provider of the Wi-Fi they're using, from being able to read the data they're sending to you as part of their relationship with your service. It may impact your service's features, too: many uses of existing JavaScript APIs require the website to be served over HTTPS. MDN has a more comprehensive list, but APIs gatewayed behind a secure context include service workers, push notifications, web share and web crypto, and some device APIs.
To serve your website over HTTPS you will need an SSL certificate. These can be created for free via Let's Encrypt, or can often be provided by your hosting service if you are using one. It is also possible to use a third-party service which "proxies" your web service and can provide HTTPS, as well as caching and CDN services. There are numerous examples of such services, such as Cloudflare and Fastly—exactly which to use depends on your current infrastructure. In the past, HTTPS could be burdensome or expensive to implement, which is why it tended to be used only on payment pages or particularly secure origins; but freely available certificates, standards improvements, and greater proliferation of browsers have removed all those obstacles.
Do
- Enable HTTPS on your servers for everything (whichever method you choose).
- Consider using a proxy in front of your servers, such as Cloudflare (httpsiseasy.com/ explains the process).
- Let's Encrypt will walk you through the process of creating your own Let's Encrypt SSL certificate.
- Or use OpenSSL directly to create your own certificate and have it signed by your choice of certificate authority (CA) (Enable HTTPS explains how to do this in detail).
Which approach you choose depends on business tradeoffs. Having a third-party manage the SSL connection for you is the easiest to set up, and does come with other benefits such as load balancing, caching, and analytics. But it also comes with an obvious ceding of some control to that third-party, and an unavoidable dependency on their services (and possible payment, depending on the services you use and your traffic levels).
Generating certificates and having them signed by a CA is how the SSL process used to be conducted, but using Let's Encrypt can be easier if it's supported by your provider or if your server team is technically adept enough for it, and it's free. It's also common for your provider to offer SSL as a service if you're using something at a higher level than cloud hosting, so it's worth checking.
Why
Security is a part of your privacy story: being able to demonstrate that you keep user data secure against interference helps to build trust. If you don't use HTTPS, your sites are also flagged as being "not secure" by browsers (and have been for some time). Existing JavaScript APIs are often only available to HTTPS pages ("secure origins"). It also protects your users against their web usage being seen by their ISP. This is certainly a best practice; there's little to no reason to not use HTTPS for websites now.
How browsers present an HTTP (not secure) page
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/V4JrNHQB0JRUVQAgljna.png", alt="Firefox HTTP URL warning.", width="543", height="307" %}
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/U1fIa4q2ng68Olf3Zdy7.png", alt="Safari desktop HTTP URL warning.", width="800", height="166" %}
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/KlN1Zd15tQxFnfLTNDgI.png", alt="Android mobile HTTP warning.", width="800", height="168" %}
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/VPffgyq9or8mPbxO2Bwp.png", alt="Apple Safari iOS HTTP warning.", width="800", height="397" %}
Redirect to HTTPS
If your site is available on both http: and https: URLs, you should redirect all http URL accesses to https. This is for the reasons above, and it also ensures that your site won't show up on whynohttps.com if it becomes popular. How to do this depends a great deal on your infrastructure. If you're hosted on AWS you can use a Classic or Application load balancer. Google Cloud is similar. In Azure you can create a Front Door; in Node with Express check for request.secure; in Nginx catch all port 80 and return 301; and in Apache, use a RewriteRule. If you're using a hosting service, it's quite likely that they'll automatically handle redirecting to HTTPS URLs for you: Netlify, Firebase, and GitHub Pages do, among many others.
HSTS
HSTS is short for "HTTP Strict-Transport-Security", and is a way of locking a browser into using HTTPS for your service forevermore. Once you're happy with your migration to HTTPS, or if you've already done that, then you can add a Strict-Transport-Security HTTP response header to your outgoing responses. A browser which has accessed your site before will record having seen this header, and from then on will automatically access this site as HTTPS even if you request HTTP. This avoids going through your redirect as above: it's as if the browser silently "upgrades" all requests to your service to use HTTPS.
Similarly, you can serve an Upgrade-Insecure-Requests
header along with your pages. This does something different from but related to Strict-Transport-Security. If you
add Upgrade-Insecure-Requests: 1, then requests from this page to other resources (images, scripts) will be requested
as https even if the link is http. However, the browser will not re-request the page itself as https, and the browser will
not remember for next time. In practice, Upgrade-Insecure-Requests is useful if you're converting an existing site with
lots of links to HTTPS and converting the link URLs in content is hard, but it's better to change the content where possible.
HSTS is primarily a security feature: it "locks" your site to HTTPS for users who have been there before. However, as above, HTTPS is good for privacy, and HSTS is good for HTTPS. Similarly, Upgrade-Insecure-Requests isn't really needed if you're updating all your content, but it is a useful “belt-and-braces” approach to add defence in depth in ensuring that your site will always be HTTPS.
Do
Add the HSTS header to your outgoing responses:
Strict-Transport-Security: max-age=300; includeSubDomains
The max-age parameter dictates how long, in seconds, the browser should remember and enforce the HTTPS upgrade. (Here we set it to 300 seconds, i.e, five minutes.) Eventually you would want this to be 6,3072,000, which is two years, and is the figure that hstspreload.org recommends, but it is quite difficult to recover if there are issues. So it is recommended that you set this with a low number at first (300), test to confirm nothing has broken, and then increase the number in stages.
Add the Upgrade-Insecure-Requests headers to your outgoing responses:
Upgrade-Insecure-Requests: 1 Content-Security-Policy: upgrade-insecure-requests
{% Aside %} Should I add HSTS/UIR headers and add redirects? Yes. You should redirect http URLs to https, and continue to do so even after HSTS is in place—it's important to do both. It's a best practice anyway (https://web.dev/redirects-http/ explains) but it's also required in order to be added to the automatic preload list (see https://hstspreload.org for more details). {% endAside %}
End-to-end encryption
A good way of keeping user data private is to not show it to anyone other than the user, including you. This helps a lot with your trust stance: if you don't have your user's data then it is clear that you can't do anything with it that they would not want. One way to do this is to not let user data leave their device at all, by storing everything client-side. This approach works, but there are limitations to a pure client-side application: browser data storage can be limited in size, and in some browsers may be cleared with little or no warning. It's also difficult or impossible to access your data across two devices, such as a laptop and a mobile phone. For this reason, it can be useful to send data to the server as normal, but encrypt it with a key known only to the user, so that the server cannot access it (because it can't decrypt it) but can store it.
How does it work?
This approach is frequently used by messaging applications, where it's referred to as "end-to-end encryption", or "e2e". In this way, two people who know one another's keys can encrypt and decrypt their messages back and forth, and send those messages via the messaging provider, but the messaging provider (who does not have those keys) can't read the messages. Most applications are not messaging apps, but it is possible to combine the two approaches—a solely client-side data store, and data encryption with a key known to the client—to store data locally but also send it encrypted to the server. It is important to realise that there are limitations to this approach: this isn't possible for all services, and in particular it can't be used if you, as the service provider, need access to what the user is storing. As described in part 2 of this series, it is best to obey the principle of data minimisation; avoid collecting data at all if you can. If the user needs data storage, but you do not need access to that data to provide the service, then end-to-end encryption is a useful alternative. If you provide services which require being able to see what the user stores to provide the service, then end-to-end encryption is not suitable. But if you do not, then you can have the client-side JavaScript of your web service encrypt any data it sends to the server, and decrypt any data it receives.
An example: Excalidraw
Excalidraw does this and explains how in a blog post. It is a vector
drawing app that stores drawings on the server, which are encrypted with a randomly chosen key. Part of the reason that
Excalidraw can implement this end-to-end encryption with relatively little code is that cryptographic libraries are now built
into the browser with window.crypto, which is a set
of JavaScript APIs supported in all modern browsers. Cryptography is hard and implementing
the algorithms comes with many edge cases. Having the browser do the heavy lifting here makes encryption more accessible to
web developers and therefore makes it easier to implement privacy via encrypted data. As Excalidraw describes in their writeup,
the encryption key remains on the client-side, because it's part of the URL fragment: when a browser visits a URL
https://example.com/path?param=1#fraghere, the path of the URL (/path) and the parameters (param=1) are passed to the server
(example.com), but the fragment (fraghere) is not, and so the server never sees it. This means that even if the encrypted
data goes through the server, the encryption key does not and thus privacy is preserved because the data is end-to-end encrypted.
Limitations
This approach to encrypting user data is not foolproof. It contributes to your trust stance for your users but it cannot fully replace it. Your users will still have to trust your service, because you could, at any moment, swap out client-side JavaScript for some subtly similar JavaScript that does not impenetrably encrypt data; and although it is possible as a user to detect whether a website you're using has done that, it's extremely difficult to do so. In practice, your users will still need to trust that you will not deliberately read and abuse their data while promising to not do so. However, demonstrating that data is encrypted and not readable by you as a (not malicious) service provider can contribute a lot to demonstrating why you are trustworthy.
It's also important to remember that one of the goals of end-to-end encryption is to stop you, the site owner, from being able to read the data. This is good for privacy, but it also means that if there are problems, you can't help. In essence, a service using end-to-end encryption puts the user in charge of the encryption keys. (This may not be obvious or overt, but someone has to have the key, and if data is kept private from you, then that's not you.) If those keys are lost, then there will be nothing you can do to help, and probably any data encrypted with those keys may also be lost. There is a fine balancing act here between privacy and usability: keep data private from everybody using encryption, but also avoid forcing users into having to be cryptology experts who manage their own keys in a secure manner.
Encryption at rest
As well as encrypting your users' data in transit, it's also important to consider encrypting data that you have stored on the server. This helps to protect against data breaches, because anyone who obtains unauthorised access to your stored data will have encrypted data, which they will hopefully not have the keys to decrypt. There are two different and complementary approaches to encrypting data at rest: encryption that you add, and encryption that your cloud storage provider adds (if you're using a cloud storage provider). The storage provider encryption doesn't provide much protection against data breaches via your software (because storage provider encryption is usually transparent to you as a user of their service), but it does help against breaches that happen at the provider's infrastructure. It's often simple to turn on and so is worth considering. This field changes rapidly and your security team (or security-savvy engineers on your team) are the best to advise on it, but all cloud storage providers offer encryption at rest for block storage Amazon S3 by setting, Azure Storage, and Google Cloud Storage by default, and for database data storage AWS RDS, Azure SQL, Google Cloud SQL among others. Check this out with your cloud storage provider, if you're using one. Handling encryption of data at rest yourself to help protect user data from data breaches is more difficult, because the logistics of securely managing encryption keys and making them available to code without also making them available to attackers is challenging. This isn't the best place to advise on security issues at that level; talk with your security-savvy engineers or dedicated team about this, or external security agencies.
Fingerprinting
Fingerprinting means trying to identify a user when they return to your website, or identifying the same user across different websites. Many characteristics may differ between your setup and someone else's. For example, you may be using a different type of device and a different browser, have a different screen size, and have different fonts installed. If I have the font "Dejavu Sans" installed and you do not, then any website can tell the difference between you and me by checking for that font. This is how fingerprinting works; you build up a collection of these data points, and each provides more ways to distinguish between users.
A more formal definition might look like this: Fingerprinting is the action of using obvious and non-obvious long-lived characteristics of a user's setup to attempt to distinguish them from as many other users as possible.
Why fingerprinting hinders user privacy
There are some edge cases where fingerprinting a user is important: fraud detection, for example. But fingerprinting can also be used to track users across sites, and that tracking is often done without users consenting to it, or, in some cases, on the basis of an invalid consent that does not adequately inform the user. When that's done, those users will often find this somewhat disquieting and feel rather betrayed.
Fingerprinting means finding ways to covertly distinguish one user from another. Fingerprinting can be used to recognize that it's still the same user on the same website, or to recognize the same user in two different browser profiles at the same time. This means that fingerprinting can be used for tracking users across sites. The deterministic and overt methods of tracking, such as storing a cookie with a unique user-specific ID, can be to some extent observed by users and controlled (and the previous module explained some of these approaches). But fingerprinting is more difficult to avoid exactly because it is covert; it relies on unchanging characteristics and most likely happens invisibly. This is why it's called "fingerprinting". It is difficult at best to change your fingerprint, whether your digital one, or the ones on the ends of your fingers.
Browser vendors know that users do not like being tracked, and are continually implementing features to limit fingerprinting (some of which we saw in the previous module). Here, we're looking at how these features may affect your business requirements and how to still do what you want to do in a privacy-protecting way. This is more about how browser protection against fingerprinting will affect what you do and how, rather than about how it will stop you doing fingerprinting.
In practice, most developers and most businesses have no need to fingerprint users. If your app requires users to sign in, then your users are identifying themselves to you, with their consent, and in a way that they can unilaterally opt out of at any time they choose. This is a privacy-protecting method of understanding which users are logged in. Your app may not require users to sign in at all, which is even more protective of your users' privacy (and you are then collecting just the data you need).
Do
Assess your third parties for fingerprinting. As part of the third parties module, you may already have a list of any third-party services you're including and the web requests that they make. It may be possible to inspect those requests to see which data is being passed back to the originator, if any. However, this is often difficult; fingerprinting is by nature a covert process which involves requesting data that isn't subject to user approval.
It's worth also reading the privacy policies of your third-party services and dependencies to look for indications of fingerprinting in use. It's sometimes referred to as "probabilistic matching", or as part of a suite of probabilistic matching techniques, as opposed to "deterministic matching".
How fingerprinting works
It can be helpful to look at https://amiunique.org/ and the EFF's Cover Your Tracks for an example of fingerprinting. Each will attempt to show you the characteristics of your personal browser setup which may differ from other people's setups. Frequently your personal combination of all these attributes is unique to you, or at least to a small group of similar people; this can be used to covertly track you. These websites aren't the whole story, and any uniqueness estimates are often how distinguishable you are among their visitors (who skew more technical and more privacy-focused) rather than among the whole internet, but the principles are sound.
Aside: passive and active fingerprinting
There is a useful distinction here to be drawn between passive and active fingerprinting techniques. A passive fingerprinting technique is one which uses information that is given to the website by default; an active fingerprinting technique is one which explicitly interrogates the browser for extra information. The reason this distinction is important is that browsers can attempt to detect and intercept, or mitigate, active techniques. APIs can be restricted, or gatewayed behind a dialog asking the user's permission (and therefore alerting the user that they're being used, or allowing the user to deny them by default). A passive technique is one which uses data that's already been given to the website, often because historically, in the nascent days of the information superhighway, that information was given to all sites. The user-agent string is an example of this, and we'll look at this in more detail further on. It was considered useful in providing quite a lot of information about the user's browser, version, and operating system, in order that a website could choose to present different things based on that. However, this also increases the amount of distinguishing information made available, information which helps identify one user from another; and so such information is increasingly no longer available, or at least frozen so that it is no longer distinguishing. If what you do relies on this information—for example, if you're taking different code branches depending on the user-agent—then this code may break as browsers increasingly freeze or stop that information. Testing is the best defence here ( see later).
Aside: measuring fingerprintability
The technical measure for how much information each of these data points provides is called entropy and is measured in bits. A feature where there are many different possible values (such as the list of fonts installed) can contribute a lot of bits to the total, whereby something without much distinguishing power (such as which operating system you're using) may only add a few. The HTTP Almanac describes how existing fingerprinting libraries automate this process of combining the responses from many different APIs into a "hash", which may identify only a small group of users, perhaps even only one. Maud Nalpas covers this in some detail in this YouTube video, but, in short, imagine that you were to see a list of your friends with their favourite music, favourite food, and the languages they speak … but with their names removed. It is quite likely that any one person's list will uniquely identify them among your friends, or at least narrow down the list to only a few people. This is how fingerprinting works; the list of things you like becomes the "hash". With that hash, identifying a user as the same person on two different unconnected sites becomes easier, which is the goal of tracking: to circumvent the user's desire for privacy.
What do browsers do against fingerprinting?
Importantly, browser vendors are very aware of many different ways for a website (or a third party included on a website) to calculate a distinguishing fingerprint for a user, or for differing bits of information to contribute to the uniqueness of that fingerprint. Some of these ways are explicit and deliberate, such as the browser's user-agent string, which often identifies the browser, operating system, and version in use (and so contributes to distinguishing you from me, if you and I are using different browsers). Some of the ways are not deliberately created to be fingerprintable but end up being so anyway, such as the list of fonts, or video and audio devices available to the browser. (The browser doesn't have to use these devices, just get a list of them by name.) And some have been established to be contributors to a fingerprint well after release, such as the exact pixel rendering of antialiasing of fonts on a canvas element. There are many more, and each way in which your browser differs from mine adds entropy and so potentially contributes to a way to tell the difference between you and me, and identify an individual person as uniquely as possible across websites. https://amiunique.org has a long (but definitely not comprehensive) list of potential fingerprint-contributing features, and the list grows all the time (as there is heavy monetary interest in being able to fingerprint users, even if the users don't want that or perhaps don't expect it).
Not supporting certain powerful APIs
The response from browser vendors to all of these approaches to calculating a user's fingerprint is to find ways to reduce the amount of entropy available from these APIs. The most restrictive option is to not implement them in the first place. This has been done by some major browsers for a variety of hardware and device APIs (such as NFC and Bluetooth access from client-side web apps), with fingerprinting and privacy concerns cited as reasons why they have not been implemented. This, obviously, can affect your apps and services: you can't use an API at all in a browser that doesn't implement it, and this can restrict or entirely cut off some hardware approaches from consideration.
The user permissions gateway
A second approach taken by browser vendors is to prevent API or data accesses without some sort of explicit user permission. This approach is often taken for security reasons as well—a website should not be able to take pictures with your webcam without your permission! But here, privacy and security can have similar interests. Identifying someone's location is a privacy violation in itself, of course, but it's also a contributor to the uniqueness of a fingerprint. Requiring permission to see geolocation does not decrease the extra entropy that a location adds to the uniqueness of that fingerprint, but it basically eliminates using geolocation for fingerprinting because it's no longer being done invisibly. The whole point of fingerprinting is to surreptitiously distinguish users from one another. If you are prepared for the user to know that you're trying to identify them, then you don't need fingerprinting techniques: have the user create an account and log in with it.
Adding unpredictability
A third approach taken in some cases is for browser vendors to "fuzz" the responses from APIs to make them less granular
and thus less identifying. This was described as part of the randomized response mechanism in the data module as something
you can do when collecting data from users, to avoid inadvertently collecting data which is identifying. Browser vendors
can take this approach to API data made available to web apps and third parties as well. An example of this is the
very accurate timing APIs used to measure page performance
from window.performance.now(). The browser knows these values
to microsecond accuracy, but the values returned are deliberately reduced in precision by rounding to the nearest 20 microsecond
boundary to avoid their use in fingerprinting (and also for security to avoid timing attacks, admittedly). The goal here is
to ensure the APIs remain useful, but to make the responses less identifying: in essence, to provide "herd immunity" by making
your device look more like everyone else's device rather than being peculiar to you. Safari presents a simplified version of the system configuration
for this very reason.
Enforcing a privacy budget
The Privacy Budget is a proposal that suggests that browsers estimate the information revealed by each fingerprinting surface. It hasn't been implemented yet in browsers. The goal is to allow powerful APIs while preserving user privacy. Learn more about the privacy budget proposal.
Use a broad testing environment
All of these will affect how you build apps and services. In particular, there is a wide diversity of responses and approaches across browsers and platforms in this area. This means that testing your work in multiple different environments is critical. This is, of course, always important, but it can be reasonable to assume that HTML rendering or CSS will be constant for a given rendering engine, no matter which browser or platform that engine is in (and so it can be tempting to test in only one Blink-based browser, for example). This is emphatically not the case for API usage precisely because browsers that share a rendering engine may differ dramatically on how they harden their API surface against fingerprinting.
Do
- Check your own analytics and audience to guide the set of browsers you should prioritize when testing.
- A good set of browsers to test in are Firefox, Chrome, Edge, Safari on desktop, Chrome and Samsung Internet on Android, and Safari on iOS. This ensures you test across the three major rendering engines (Gecko in Firefox, various forks of Blink in Chrome, Edge, and Samsung Internet, and Webkit in Safari), and on both mobile and desktop platforms.
- If your site may also be used on less common devices, such as tablets, smartwatches, or gaming consoles, test there as well. Some hardware platforms can lag behind mobile and desktop for browser updates, meaning that some APIs may be unimplemented or unavailable in the browsers on those platforms.
- Test with one or more browsers which claim user privacy as a motivator. Include upcoming pre-release and test versions of your most common browsers where you can and if they're available to you: Safari's technology preview, Chrome's Canary, Firefox's Beta channel. These give you the best chance of identifying API breakage and changes that affect your sites before those changes affect your users. Similarly, bear your users' environments in mind with reference to any analytics you have present. If your user base has high numbers of older Android phones, be sure to include those in your tests. Most people do not have the fast hardware and latest releases that a development team does.
- Test using both a clean profile and in incognito/private browsing mode; the likelihood is that you have already granted the permissions required in your personal profile. Test what happens if you deny permission to the site for any question.
- Explicitly test your pages in Firefox's fingerprinting protection mode. Doing so will show permission dialogues if your page is attempting fingerprinting, or will return fuzzed data for some APIs. This helps you confirm if third parties included in your service are using fingerprintable data, or if your own service depends on that. You can then consider whether the deliberate fuzzing makes it more difficult to do what you need. Consider making corrections accordingly to obtain that data from another source, do without it, or use less granular data.
- As previously discussed in the third-parties module, it's also important to audit your third-party
dependencies to see if they are using fingerprinting techniques. Passive fingerprinting is difficult to detect (and
impossible if a third party does it on their server) but fingerprinting mode may flag some fingerprinting techniques,
and looking for uses of navigator.userAgent or unexpected creation of
<canvas>objects may also reveal some approaches that deserve further scrutiny. It is also worth looking for uses of the term "probabilistic matching" in marketing or technical material describing a third party; this can sometimes indicate the use of fingerprinting techniques.
Cross-browser testing tools
Testing your code for privacy purposes is difficult to automate, and the things to look for when testing manually are described earlier. What happens when you deny permission to the site for any APIs it tries to access, for example, and how is that presented to the user? An automated test cannot judge whether the site acts so as to help the user trust it, or conversely to encourage the user to distrust it, or think that something is being hidden.
However, once the site has been audited, the testing of APIs to confirm that nothing has broken in newer browser versions (or in upcoming "beta" and "preview" versions) can be automated, and largely should be as part of your existing test suite. Something to consider with your automated test tools, when working with API surface coverage, is that most browsers allow some level of control over which APIs and features are available. Chrome does so through command line switches, as does Firefox, and having access to these in the testing tool setup will allow you to run certain tests with APIs turned off or on. (See, for example, Cypress's browser-launch plugin a nd puppeteer's launch.args parameter for ways to add browser flags when launching.)
Only rely on the user-agent string for coarse information
Taking another example, browsers have since the beginning of the web sent a description of themselves with every request in the HTTP User-Agent header. For nearly as long, people have been exhorting web developers to not use the contents of the user-agent header to serve different content to different browsers, and for all that time web developers have done it anyway, with a certain amount of justification in some (but not all) cases. Since browsers don't want to be singled out for a suboptimal experience by websites, this results in every browser pretending to be every other browser, and the user-agent string looking something like:
Mozilla/5.0 (Linux; Android 6.0.1; SGP771 Build/32.2.A.0.253; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/52.0.2743.98 Safari/537.36.
This claims, among other things, to be Mozilla/5.0, a browser which was released at the same time that the first astronauts boarded the International Space Station over two decades ago. The user-agent string is a rich source of entropy for fingerprinting, of course, and to mitigate that fingerprintability, browser manufacturers have either already frozen the user-agent header or are working towards doing so. This is another example of changing the data that an API provides without necessarily removing the API entirely. Sending a blank user-agent header would break countless websites which assume that it is present. So in general, what browsers are doing is removing some of the detail from it, and then keeping it mostly unchanged from then on. (You can see this happening in Safari, Chrome, and Firefox.) This protection against detailed fingerprinting essentially means that you cannot rely on the user-agent header being accurate anymore, and if you are doing so then it's important to find alternative data sources.
To be clear, the data in the user-agent isn't going entirely away, but it is available at a lower granularity, or is sometimes inaccurate because an older but unchanging number may be reported. For example, Firefox, Safari, and Chrome all cap the reported macOS version number to ten (see Update on user-agent string reduction for more discussion here). The exact details for how Chrome plans to reduce data in the user-agent string are available at User-Agent Reduction but, in short, you can expect that the reported browser version number will only contain a major version (so the version number will look like 123.0.0.0, even if the browser is version 123.10.45.108), and the OS version will be without detail and will freeze to one of a small number of unchanging choices. So an imaginary Chrome version 123.45.67.89 running on an imaginary "Windows 20" would report its version number as:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
The core information you need (the browser version) is still available: it is Chrome 123, on Windows. But the subsidiary information (the chip architecture, which Windows version, which version of Safari it's imitating, the browser minor version) will no longer be available after the freeze.
Compare this with a "current" Chrome user-agent on a different platform:
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36,
and it can be seen that the only thing that differs is the Chrome version number (104) and the platform identifier.
Similarly, Safari's user-agent string does show a platform and a Safari version number, and also gives an OS version on iOS, but all else is frozen. So an imaginary Safari version 1234.5.67 running on an imaginary macOS 20 might give its user-agent as:
Mozilla/5.0 (Macintosh; **Intel Mac OS X 10_20_0**) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15,
and on an imaginary iOS 20 it might be:
Mozilla/5.0 (iPhone; CPU **iPhone OS 20_0** like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/**20.0 Mobile/15E148 Safari/605.1.15**.
Again, the core information (this is Safari, it's on iOS or macOS) is available, and iOS Safari still provides an iOS version number; but much of the ancillary information that was available in the past has since been frozen. Importantly, this includes the Safari version number, which is not necessarily available.
The changes to the reported user-agent were hotly debated. https://github.com/WICG/ua-client-hints#use-cases summarises summarises some of the arguments and reasons for the change, and Rowan Merewood has a slide deck with some strategies for migrating away from using the user-agent for differentiation, in the context of the UA Client Hints proposal explained further on.
Fuzzing
Fuzzing is a term from security practice, where APIs are called with unexpected values in the hope that they handle those
unexpected values badly and expose a security issue. Web developers should be familiar with cross-site scripting (XSS),
which involves adding malicious script to a page, often because the page doesn't correctly escape injected HTML (so you do a search query
with the text <script> in it). Back-end developers will be aware of SQL injection,
where database queries that don't correctly validate user input expose security issues (as notably illustrated by xkcd with
Little Bobby Tables). Fuzzing, or fuzz testing, is more properly
used for automated attempts to provide many different invalid or unexpected inputs to an API and to check the results for security leaks,
crashes, or other bad handling. These are all examples of deliberately providing inaccurate information. Here, though, it's being done
preemptively by browsers (by making the user-agent deliberately incorrect), to encourage developers to stop relying on that data.
Do
- Check your codebase for any reliance on the user-agent string (a search for
navigator.userAgentis likely to find most occurrences in your client-side code, and your backend code will likely be looking forUser-Agentas a header), including your dependencies. - If you find uses in your own code, work out what the code is checking for, and find another way to do that differentiation (or find a replacement dependency, or work with the dependency upstream by filing issues or checking with them for updates). Sometimes browser differentiation is necessary to work around bugs, but the user-agent will increasingly become not the way to do this once it is frozen.
- You may be safe. If you’re only using the core values of brand, major version, and platform, then these will almost certainly still be available and be correct in the user-agent string.
- MDN describes good ways to avoid reliance on the user-agent string ("browser sniffing"), the main one of which is feature detection.
- If you are reliant on the user-agent string in some way (even when using the few core values that remain useful), it’s a good idea to test with upcoming user-agents that will be in new browser releases. It is possible to test with those upcoming browser versions themselves by means of beta or technology preview builds, but it's also possible to set a custom user-agent string for testing. You can override the user-agent string in Chrome, Edge, Firefox, and Safari, when doing local development, in order to check how your code deals with different user-agent values that you might receive from users.
Client Hints
One major proposal to provide this information is User-Agent Client Hints,
although this is not supported across all browsers. Supporting browsers will pass three headers: Sec-CH-UA, which gives
a browser brand and version number; Sec-CH-UA-Mobile, which indicates if the request comes from a mobile device; and Sec-CH-UA-Platform,
which names the operating system. (Parsing these headers is less easy than it seems because they are
Structured Headers rather than simple strings,
and this is enforced by browsers sending "tricky" values, which will be handled incorrectly if not properly parsed. This is,
as earlier, an example of “fuzz testing” being done preemptively by the browser. A developer using this data is required to handle
it properly because the data is designed so that improper or lazy parsing will likely give bad results, such as showing brands that don’t
exist, or strings that don’t close properly.) Fortunately, this data is also made available by the browser to JavaScript directly as
navigator.userAgentData, which in a supporting browser might look something like this object:
{
"brands": [
{
"brand": " Not A;Brand",
"version": "99"
},
{
"brand": "Chromium",
"version": "96"
},
{
"brand": "Google Chrome",
"version": "96"
}
],
"mobile": false
}
Third parties
It's rather rare that a website is wholly self-contained. The HTTP Web Almanac shows that most websites—around 95%—contain some third-party content.
The Almanac defines third-party content as something hosted on a shared and public origin, which is widely used by a variety of sites and uninfluenced by an individual site owner. These might be images or other media such as video, fonts, or scripts. Images and scripts account for more than everything else added together. Third-party content isn't essential for developing a site, but it might as well be; you will almost certainly be using something loaded from a public shared server, whether web fonts, embedded iframes of videos, adverts, or JavaScript libraries. For example, you may be using web fonts served from Google Fonts, or measuring analytics with Google Analytics; you may have added Like buttons or Sign In With buttons from social networks; you may be embedding maps or video, or handling shopping purchases via third-party services; you may be tracking errors and logging for your own development teams via third-party monitoring tools.
For privacy purposes, it is useful to consider a slightly different and less broad definition: a third-party resource, and in particular a third-party script, is served from a shared and public origin and widely used as illustrated, but is also authored by someone other than the site owner. The aspect of third-party authorship is what's key when considering how to protect your users' privacy from others. This will lead you to consider what risks are present, and then decide how or whether to use a third-party resource based on those risks. As already discussed, these things will help you to understand the context and therefore to understand which trade-offs you need to make, and what they mean.
This isn't quite what's meant when discussing "third-party resources" in general: the distinction between first-party and third-party is really one about the context in which something is used. A script loaded from another website is a third-party resource, and the HTTP request which loads the script may include cookies, but those cookies aren't really "third-party cookies"; they're just cookies, and whether they're "third-party" or "first-party" depends on whether the script is being loaded on a page on your site or a page on the script owner's site.
Why do we use third-party resources?
Third-parties are a great way to add functionality to your site. That may be features that are exposed to users, or invisible developer functions such as error tracking, but they reduce your development load, and the scripts themselves are maintained by someone else: the development team of the service you're including. This all works because of the composability of the web: being able to put parts together to form a whole that is greater than their sum.
The HTTP Archive's Web Almanac gives a nice description:
Third parties provide a never-ending collection of images, videos, fonts, tools, libraries, widgets, trackers, ads, and anything else you can imagine embedding into our web pages. This enables even the most non-technical to be able to create and publish content to the web. Without third parties, the web would likely be a very boring, text-based, academic medium instead of the rich, immersive, complex platform that is so integral to the lives of many of us today.
What can third-party resources do?
Accessing some information
When you use a third-party resource on your website, regardless of what it is, some information is passed to that third party. For example, if you include an image from another site, the HTTP request that the user's browser makes will pass along a Referer header with your page's URL, as well as the user's IP address.
Cross-site tracking
Continuing with that same example—when the image loads from the third-party site, it can include a cookie, and that cookie will be sent back to the third-party when the user next requests that image. This means that the third party can know that their service is being used on your site, and it can send back a cookie, potentially with a unique ID for that user. This means that the next time the user visits your site, or any other site which includes a resource from that third party, that unique ID cookie will be sent again. This allows the third party to build up a log of where that user visits: your site, other sites that use the same third-party resource, all over the web.
This is cross-site tracking: allowing a third party to collect a log of a user's activity on many websites, as long as those websites all use a resource coming from the same third party. That might be a font, an image, or a stylesheet—all static resources. It also might be a dynamic resource: a piece of script, a social media button, an advert. Included script can gather even more information, because it is dynamic: it can inspect the user's browser and environment and pass that data back to its originator. Any script can do this to some extent, as can dynamic resources that don't present as a script, such as a social media embed or an ad or a share button. If you look at the details of a cookie banner on popular websites you can see a list of the organizations that may add a tracking cookie to your users to build up a picture of their activities to create a profile of that user. There can be hundreds of them. If a third party offers a service for free, one way that this can be economically viable for them to do is because they collect and then monetise this data.
A useful guide to the types of privacy issues from which a browser should protect its users is the Target Privacy Threat Model. This is a document still under discussion at the time of writing, but it gives some high-level classifications of the sorts of privacy threats that exist. The risks from third-party resources are primarily "unwanted cross-site recognition", where a website can identify the same user across multiple sites, and "sensitive information disclosure", where a site can collect information a user considers sensitive.
This is a key distinction: unwanted cross-site recognition is bad even if the third party isn't gathering additional sensitive information from it, because it takes away a user's control over their identity. Getting access to a user's referrer and IP address and cookies is an unwanted disclosure in itself. Using third-party resources comes along with a planning component of how you'll use them in a privacy-preserving way. Some of that work is in your remit as the site developer, and some is done by the browser in its role as the user agent; that is, the agent working on behalf of the user to avoid sensitive information disclosure and unwanted cross-site recognition where possible. Below we'll look in more detail at mitigations and approaches on a browser level and a site development level.
Server-side third-party code
Our earlier definition of a third party deliberately altered the HTTP Almanac's rather client-side approach (as is appropriate for their report!), to include third-party authorship, because from a privacy perspective, a third party is anyone who knows anything about your users who isn't you.
This does include third parties who provide services that you use on the server, as well as the client. From a privacy standpoint, a third-party library (such as something included from NPM or Composer or NuGet) is also important to understand. Do your dependencies pass data outside your borders? If you pass data to a logging service or a remotely hosted database, if libraries you include also "phone home" to their authors, then these may be in a position to violate your users' privacy and therefore they need to be audited. A server-based third party usually has to be handed the user data by you, which means that the data it is exposed to is more under your control. By contrast, a client-based third party—a script or HTTP resource included on your website and fetched by the user's browser—can collect some data directly from the user without that process of collection being mediated by you. Most of this module will be concerned with how to identify those client-side third parties you've elected to include and expose your users to, exactly because there is less mediation possible by you. But it is worth considering securing your server-side code so that you understand outbound communications from it and can log or block any that are unexpected. Details on how exactly to do this are outside our scope here (and very dependent on your server setup) but this is another part of your security and privacy stance.
Why do you need to be careful with third parties?
Third-party scripts and features are really important, and our goal as web developers should be to integrate such things, not to turn away from them! But there are potential problems. Third-party content may create performance issues and can also create security issues because you're bringing in an external service inside your trust boundary. But third-party content can also create privacy issues!
When we're talking about third-party resources on the web, it's useful to think of security issues being (among other things) where a third party can steal data from your company, and to contrast that with privacy issues, which are (among other things) where a third party that you included steals or obtains access to your users' data without you (or them) consenting.
An example of a security issue is where "web skimmers" steal credit card information-–a third-party resource which is included on a page into which a user enters credit card details can, potentially, steal those credit card details and send them off to the malicious third party. Those who create these skimmer scripts are very creative in working out places to hide them. One summary describes how skimmer scripts have been hidden in third-party content such as those images used for site logos, favicons, and social media networks, popular libraries such as jQuery, Modernizr, and Google Tag Manager, site widgets such as live chat windows, and CSS files.
Privacy issues are a little different. These third parties are part of your offering; to maintain your users' trust in you, you need to be confident that your users can trust them. If a third party that you use gathers data about your users and then misuses it, or makes it difficult to delete or discover, or suffers a data breach, or violates your users' expectations, then your users will likely perceive that as a breakdown in their trust of your service, not simply of the third party. It is your reputation and relationship on the line. This is why it's important to ask yourself: do you trust the third parties you're using on your site?
What are some examples of third parties?
We're discussing "third parties" generally, but there are actually different kinds, and they have access to different amounts of user data.
For example, adding an <img> element to your HTML, loaded from a different server, will give that server different information
about your users than adding an <iframe> might, or a <script> element. These are examples rather than a comprehensive list, but it's
useful to understand the distinctions between the different types of third-party items that your site can employ.
Requesting a cross-site resource
A cross-site resource is anything on your site which is loaded from a different site and isn't an <iframe> or a <script>. Examples
include <img>, <audio>, <video>, web fonts loaded by CSS, and WebGL textures. These are all loaded via an HTTP request, and as
described earlier, those HTTP requests will include any cookies previously set by the other site, the requesting user's IP address,
and the URL of the current page as the referrer. All third-party requests historically included this data by default, although
there are efforts to reduce or isolate the data passed to third parties by various browsers, as described in "Understanding
Third-Party Browser Protections" further on.
Embedding a cross-site iframe
A complete document embedded in your pages via an <iframe> can request additional access to browser APIs, in addition to the trifecta
of cookies, IP address, and referrer. Exactly which APIs are available to <iframe>d pages and how they request access is browser-specific,
and is currently undergoing changes: see "Permissions Policy" below for current efforts to curtail or monitor API access in embedded
documents.
Executing cross-site JavaScript
Including a <script> element loads and runs cross-site JavaScript in the top-level context of your page. This means that the
script that runs has complete access to anything that your own first-party scripts do. Browser permissions still manage this data,
so requesting the user's location (for example) will still require user agreement. But any information present in the page or
available as JavaScript variables can be read by such a script, and this includes not just cookies that are passed to the third party
as part of the request, but also cookies that are intended for your site alone. Equally, a third-party script loaded onto your
page can make all the same HTTP requests that your own code does, meaning that it could make fetch() requests to your back-end APIs to get data.
Including third-party libraries in your dependencies
As described earlier, your server-side code will also likely include third-party dependencies, and these are indistinguishable from your own code in their power; code that you include from a GitHub repository or your programming language's library (npm, PyPI, composer, and so on) can read all the same data that your other code can.
Knowing your third parties
This, then, requires some understanding of your list of third-party suppliers, and what their privacy, data collection, and user experience stances and policies are. That understanding then becomes part of your series of trade-offs: how useful and important is the service, balanced against how intrusive, inconvenient, or disquieting their demands are on your users. Third-party content provides value by taking the heavy lifting from you as site owner and allows you to focus on your core competencies; and so there is value in making that trade-off and sacrificing some user comfort and privacy for a better user experience. It is important to not confuse user experience with developer experience, though: "it is easier for our dev team to build the service" is not a compelling story to users.
How you get that understanding is the process of audit.
Audit your third parties
Understanding what a third party does is the process of audit. You can do this both technically and non-technically, and for an individual third party and for your whole collection.
Run a non-technical audit
The first step is non-technical: read the privacy policies of your suppliers. If you include any third-party resources, check the privacy policies. They will be long and full of legal text, and some documents may use some of the approaches specifically warned against in earlier modules, such as overly general statements and without any indication of how or when collected data will be removed. It is important to realize that from a user perspective, all the data that is collected on your site, including by third parties, will be governed by these privacy policies. Even if you do everything correctly, when you are transparent about your goals and exceed your users' expectations of data privacy and sensitivity, users may hold you responsible for anything your chosen third parties do as well. If there is anything in their privacy policies which you would not wish to say in your own policies because it would decrease users' trust, then consider whether there is an alternative supplier.
This is something which can usefully go hand in hand with the technical audit discussed further on, as they inform one another. You should know the third-party resources that you're incorporating for business reasons (such as ad networks or embedded content) because there will be a business relationship in place. This is a good place to start a non-technical audit. The technical audit is also likely to identify third parties, especially those included for technical rather than business reasons (external components, analytics, utility libraries), and that list can join with the list of business-focused third parties. The goal here is for you as site owner to feel that you understand what the third parties you're adding to your site are doing, and for you as a business to be able to present your legal advisor with this inventory of third parties to make sure you are meeting any obligations required.
Run a technical audit
For a technical audit, it's important to use the resources in situ as part of the website; that is, don't load a dependency in a test harness and inspect it that way. Make sure you're seeing how your dependencies act as part of your actual website, deployed on the public internet rather than in a test or development mode. It is very instructive to view your own website as a new user. Open a browser in a clean new profile, so that you are not signed in and have no stored agreement, and try visiting your site.
{% Aside %} Testing a site as if you are a new user means viewing that site with no saved information about it. It is best in these situations to create a brand new profile every time you want to test a website as though you're a new user. Don't create one new profile and re-use it; create a new profile every time and then discard it. This is mostly a quick process on desktop browsers: see the documentation for Chrome, Edge, and Firefox. Chrome for Android and Safari don't support creating a new empty profile, so incognito/private browsing modes will have to suffice there, but in general, it is important to test sites both in incognito/private browsing modes and with a freshly-created profile. {% endAside %}
Sign up on your own site for a new account, if you provide user accounts. Your design team will have orchestrated this new user
acquisition process from a UX perspective, but it can be illustrative to approach it from a privacy perspective. Don't simply click
"Accept" on the terms and conditions, or the cookie warning, or the privacy policy; set yourself the task of using your own service
without disclosing any personal information or having any tracking cookies, and see if you can do it and how hard it is to do it.
It can also be helpful to look in the browser developer tools to see which sites are being accessed and which data is passed to
those sites. Developer tools provide a list of separate HTTP requests (normally in a section called "Network"), and you can see
from here requests grouped by type (HTML, CSS, images, fonts, JavaScript, requests initiated by JavaScript). It's also possible
to add a new column to show the domain of each request, which will demonstrate how many different places are being contacted,
and there may be a "third-party requests" checkbox to show only third parties. (It can also be useful to use Content-Security-Policy
reporting to perform a continual audit, for which read further on.)
Simon Hearne's "Request Map Generator" tool can also be a helpful overview of all the subrequests that a publicly available page makes.
This is also the point at which you can include the business-focused third parties identified as part of the non-technical audit (that is: the list of companies that you have a financial relationship with in order to use their resources). The goal here is to match up the list of third parties you believe you're using (from financial and legal records) and the list you're actually using (by looking at which third-party HTTP requests your website makes). You should be able to identify for each business third party which technical outbound requests are made. If you cannot identify requests in the technical audit for a third party identified by business relationship, it's important to work out why and to have that guide your testing: perhaps that third-party resource is only loaded in a particular country, or on a particular device type, or for logged-in users. This will enlarge your list of site areas to audit and ensure that you're seeing all the outbound accesses. (Or possibly it will identify a third-party resource that you're paying for and not using, which always cheers up the finance department.)
Once you've narrowed down the list of requests to third parties that you would like to be part of the audit, clicking on an individual request will show all the details of it and, in particular, which data was passed to that request. It is also very common that a third-party request that your code initiates then goes on to initiate many other third-party requests. These additional third parties are also "imported" into your own privacy policy. This task is laborious but valuable, and it often can be inserted into existing analyses; your frontend development team should already be auditing requests for performance reasons (perhaps with the help of existing tools such as WebPageTest or Lighthouse), and incorporating a data and privacy audit into that process can make it easier.
Do
Open a browser with a clean new user profile, so that you are not signed in and have no stored agreement; then open the browser development tools Network panel to see all the outgoing requests. Add a new column to show the domain of each request, and check the "third-party requests" checkbox to show only third parties if present. Then:
- Visit your site.
- Sign up for a new account, if you provide user accounts.
- Try to delete your created account.
- Carry out a normal action or two on the site (exactly what this is will depend on what your site does, but pick common actions that most users perform).
- Carry out an action or two which you know involve third-party dependencies in particular. These might include sharing content to social media, beginning a checkout flow, or embedding content from another site.
When doing each of these tasks, make a record of resources requested from domains that are not yours, by looking at the Network panel as described. These are some of your third parties. A good way to do this is to use the browser network tools to capture the network request logs in a HAR file.
HAR files and capturing
An HAR file is a standardized JSON format of all network requests made by a page. To get an HAR file for a particular page, in:
Chrome
Open the browser DevTools (Menu > More Tools > Developer Tools), go to the Network panel, load (or refresh) the page, and choose the downward arrow save symbol in the top right near to the No throttling dropdown menu.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/pe6kjowLV339yjfaAgkr.png", alt="The Chrome DevTools network panel with Download HAR symbol highlighted.", width="650", height="299" %}
Firefox
Open the browser developer tools (Menu > More Tools > Web Developer Tools), go to the Network panel, load (or refresh) the page, and choose the cog symbol in the top right next to the throttling dropdown menu. From its menu, choose Save All As HAR**.
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/SIeaTTW90Zh6TOV8RTKa.png", alt="The Firefox developer tools network panel with the Save All As Har option highlighted.", width="624", height="237" %}
Safari
Open the browser developer tools (Menu > Develop > Show Web Inspector; if you don't have a Develop menu then enable it from Menu > Safari > Preferences > Advanced > Show Develop menu in menu bar), go to the Network panel, load (or refresh) the page, and choose Export in the top right (to the right of Preserve Log; you may need to enlarge the window).
{% Img src="image/cGQxYFGJrUUaUZyWhyt9yo5gHhs1/PDUiriJh7D8FpL1v3RwD.png", alt="Safari Web Inspector Network panel with the HAR export option highlighted.", width="800", height="266" %}
For more detail, you can also record what is passed on to third parties (in the Request section), although this data is often obfuscated and not usefully interpretable.
Best practices when integrating third parties
You can set your own policies on which third parties your site uses: change which ad provider you use based on their practices, or how annoying or intrusive their cookies consent popup is, or whether you want to use social media buttons on your site or tracking links in your emails or utm_campaign links to track in Google Analytics in your tweets. One aspect to consider when developing a site is the privacy and security posture of your analytics service. Some analytics services explicitly trade on being privacy-protecting. Frequently, there are also ways to use a third-party script which adds privacy protection in itself: you are not the first team looking to improve their users' privacy and protect them from third-party data collection, and there may already be solutions. Finally, many third-party providers are more sensitive to data collection issues now than in the past, and there are often features or parameters you can add which enable a more user-protective mode. Here are some examples.
When adding a social media sharing button
Consider embedding HTML buttons directly: the site https://sharingbuttons.io/ has some well-designed examples. Or you could add plain HTML links. The trade-off here is that you lose the "share count" statistic and your ability to classify your customers in your Facebook analytics. This is an example of a trade-off between using a third party provider and receiving less analytics data.
More generally, when you're embedding an interactive widget of some kind from a third party, it's often possible to instead provide a link out to that third party. This does mean that your site doesn't have the inline experience, but it shifts the decision on sharing data with the third party from you to your user, who can choose to interact or not as they prefer.
For example, you might add links for Twitter and Facebook to share your service at mysite.example.com like this:
<a href="https://facebook.com/sharer/sharer.php?u=https%3A%2F%2Fmysite.example.com"
rel="noopener" aria-label="Share on Facebook" target="_blank" >Share on Facebook</a>
<a href="https://twitter.com/intent/tweet/?text=My%20cool%20service!&url=https%3A%2F%2Fmysite.example.com"
rel="noopener" aria-label="Share on Twitter" target="_blank">Share on Twitter</a>
Note that Facebook allows specifying a URL to share, and Twitter allows specifying a URL and some text.
When embedding a video
When you're embedding videos from video-hosting sites, look for privacy-preserving options in the embedding code.
For example, for YouTube, replace youtube.com in the embed URL with www.youtube-nocookie.com to avoid tracking cookies
being placed on users viewing the embedding page. You can also check "Enable privacy-enhanced mode" when generating the
Share/Embed link from YouTube itself. This is a good example of using a more user-protective mode provided by the third party.
(https://support.google.com/youtube/answer/171780 describes this in more detail,
and other embedding options for YouTube specifically.)
Other video sites have fewer options in this regard: TikTok, for example, doesn't have a way to embed video without tracking at the time of this writing. It is possible to host the videos yourself (this is using an alternative), but it can be a lot more work, especially to support many devices.
As with interactive widgets discussed earlier, it is often possible to replace an embedded video with a link to the providing website.
This is less interactive because the video won't play inline, but it leaves the choice of whether to watch with the user. This can be
used as an example of the "facade pattern", the name for dynamically replacing interactive content with something requiring a user
action to trigger it. An embedded TikTok video can be replaced with a plain link to the TikTok video page, but with a little more
work it is possible to retrieve and display a thumbnail for the video and make that a link. Even if the chosen video provider doesn't
support an easy way to embed videos without tracking, many video hosts support oEmbed, an API that, when given
a link to a video or embedded content, will return programmatic detail for it, including a thumbnail and title. TikTok supports oEmbed
(see https://developers.tiktok.com/doc/embed-videos for details), meaning that
you can (manually or programmatically) turn a link to a TikTok video https://www.tiktok.com/@scout2015/video/6718335390845095173 into JSON metadata about that video with
https://www.tiktok.com/oembed?url=https://www.tiktok.com/@scout2015/video/6718335390845095173, and therefore retrieve a thumbnail
to display. WordPress often uses this to request oEmbed' information for embedded content, for example. You can use this programmatically
to show a "facade" that looks interactive and switches to embed or link to an interactive video when the user chooses to click on it.
When embedding analytics scripts
Analytics is designed to collect info about your users so it can be analyzed: this is what it's for. Analytics systems are essentially services to collect and display data about accesses and users, which is done on a third-party server such as Google Analytics for ease of implementation. There are also self-hosted analytics systems such as https://matomo.org/, although this is more work than using a third-party solution for this. Running such a system on your own infrastructure does help you to reduce data collection, though, because it does not leave your own ecosystem. On the other hand, managing that data, removing it, and setting policies for it becomes your responsibility. Much of the concern with cross-site tracking comes about when it's done surreptitiously and secretively, or as a side-effect of using a service which need not contain data gathering at all. Analytics software is overtly designed to collect data in order to inform the site owners about their users.
Historically, there has been an approach of gathering all the data you can about everything, like a giant fishing net, and then analyzing it later for interesting patterns. This mindset has, in large part, created the sense of unease and disquiet about data collection that was discussed in part 1 of this course. Now, many sites first work out which questions to ask and then gather specific and limited data to answer those questions.
If some third-party service is used by your site and by other sites, and it works by you including their JavaScript into your site, and it sets cookies for each user, then it's important to consider that they could be doing unwanted cross-site recognition; that is, tracking your users across sites. Some may and some may not, but the privacy-protecting stance here is to assume that such a third-party service is in fact doing cross-site tracking unless you have a good reason to think or know otherwise. This is not in itself a reason to avoid such services, but it is something to consider in your assessment of the trade-offs of using them.
The trade-off in analytics used to be solely to choose whether to use it or not: gather all data and compromise privacy in exchange for insights and planning, or give up insights entirely. However, this is no longer the case, and there is often now a middle ground to be found between these two extremes. Check your analytics provider for configuration options to limit the data collected and reduce the amount and duration of its storage. Since you have the records from the technical audit described earlier, you can re-run the relevant sections of that audit to confirm that changing these configurations does actually reduce the amount of data collected. If you're making this transition on an existing site, then this can give you some quantifiable measure that can be written about for your users. For example, Google Analytics has a number of opt-in (therefore off by default) privacy features, many of which may be helpful for complying with local data protection laws. Some options to consider when setting up Google Analytics include setting the retention period on collected data (Admin > Tracking Info > Data Retention) lower than the 26-month default, and enabling some of the more technical solutions such as partial IP anonymization (see https://support.google.com/analytics/answer/9019185 for more detail).
Using third parties in a privacy-preserving way
Thus far, we've discussed how to protect your users from third parties during the design phase of your application, while you're planning what that application will do. Deciding to not use a particular third party at all is part of this planning, and auditing your uses also falls into this category: it's about making decisions about your privacy stance. However, these decisions are inherently not very granular; choosing to use a particular third party or choosing not to is not a nuanced decision. It is much more likely that you will want something in between: to need or plan to use a particular third-party offering but mitigate any privacy-violating tendencies (whether deliberate or accidental). This is the task of protecting users at "build time": adding safeguards to reduce harm that you did not anticipate. All these are new HTTP headers that you can provide when serving pages and which will hint or command the user agent to take certain privacy or security stances.
Referrer-Policy
Do
Set a policy of strict-origin-when-cross-origin or noreferrer to prevent other sites from receiving a Referer header
when you link to them or when they're loaded as subresources by a page:
index.html:
<meta name="referrer" content="strict-origin-when-cross-origin" />
Or server-side, for example in Express:
const helmet = require('helmet');
app.use(helmet.referrerPolicy({policy: 'strict-origin-when-cross-origin'}));
If need be, set a laxer policy on specific elements or requests.
Why this protects user privacy
By default, each HTTP request the browser makes passes on a Referer header which contains the URL of the page initiating the request,
whether a link, an embedded image, or script. This can be a privacy issue because URLs can contain private information, and those URLs
being available to third parties passes that private information to them. Web.dev lists some examples
of URLs containing private data—knowing that a user came to your site from https://social.example.com/user/me@example.com tells you who that user is,
which is a definite leak. But even a URL which does not itself expose private information does expose that this particular user (who you may know,
if they're logged in) came here from another site and this therefore reveals that this user visited that other site. This is in itself exposure of
information that you maybe should not know about your user's browsing history.
{% Aside %} (It's spelled incorrectly, yes: Referrer should have a double R. People have been complaining about this for quarter of a century.) {% endAside %}
Providing a Referrer-Policy header (with correct spelling!) lets you alter this, so that some or none of the referring URL is passed on.
MDN lists the full details but most browsers have
now adopted an assumed value of strict-origin-when-cross-origin by default, meaning that the referrer URL is now passed to third
parties as an origin only (https://web.dev rather than https://web.dev/learn/privacy). This is a useful privacy protection without
you having to do anything. But you can tighten this up further by specifying Referrer-Policy: same-origin to avoid passing any
referrer information at all to third parties (or Referrer-Policy: no-referrer to avoid passing to anyone including your own origin).
(This is a nice example of the privacy-versus-utility balance; the new default is much more privacy-preserving than before, but it
still gives high level information to third parties of your choice, such as your analytics provider.)
It's also useful to explicitly specify this header because then you know exactly what the policy is, rather than relying on the browser defaults.
If you aren't able to set headers, then it's possible to set a referrer policy for a whole HTML page using a meta element in the <head>:
<meta name="referrer" content="same-origin">; and if concerned about specific third parties, it's also possible to set a referrerpolicy
attribute on individual elements such as <script>, <a>, or <iframe>: <script src="https://thirdparty.example.com/data.js" referrerpolicy="no-referrer">
Content-Security-Policy
The Content-Security-Policy header, often referred to as "CSP", dictates where external resources can be loaded from.
It is primarily used for security purposes, by preventing cross-site scripting attacks and script injection, but when used
alongside your regular audits it can also limit where your chosen third parties can pass data to.
This is potentially a less-than-great user experience; if one of your third-party scripts starts loading a dependency from an origin not on your list, then that request will be blocked, the script will fail, and your application may fail with it (or at least be reduced to its JavaScript-failing fallback version). This is useful when CSP is implemented for security, which is its normal purpose: protecting against cross-site scripting issues (and to do this, use a strict CSP). Once you know all the inline scripts that your page uses, you can make a list of them, calculate a hash value or add a random value (called a "nonce") for each, and then add the list of hashes to your Content Security Policy. This will prevent any script that isn't on the list from being loaded. This needs to be baked into the production process for the site: scripts in your pages need to include the nonce or to have a hash calculated as part of the build. See the article on strict-csp for all the details.
Fortunately, browsers support a related header, Content-Security-Policy-Report-Only. If this header is provided, requests
that violate the supplied policy will not be blocked, but a JSON report will be sent to a supplied URL. Such a header might
look like this:
Content-Security-Policy-Report-Only: script-src 3p.example.com; report-uri https://example.com/report/,
and if the browser loads a script from anywhere other than 3p.example.com, that request will succeed but a report will
be sent to the supplied report-uri. Normally this is used to experiment with a policy before implementing it, but a useful
idea here is to use this as a way of conducting an "ongoing audit". As well as your regular audit described earlier, you
can turn on CSP reporting to see if any unexpected domains appear, which could mean that your third-party resources are loading
third-party resources of their own and which you need to consider and evaluate. (It may also be a sign of some cross-site
scripting exploits having slipped past your security boundary, of course, which it is also important to know about!)
Content-Security-Policy is a complex and fiddly API to use. This is known, and there is work going on to build the "next generation" of CSP
which will meet the same goals but not be quite as complicated to use.This isn't ready yet, but if you'd like to see where this is heading
(or to get involved and help in its design!) then check out https://github.com/WICG/csp-next for details.
Do
Add this HTTP header to pages served: Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://a-url-you-control.
When JSON is posted to that URL, store it. Review that stored data to get a collection of the third-party domains that your website requests when visited by others.
Update the Content-Security-Policy-Report-Only header to list the domains you expect, in order to see when that list changes:
Content-Security-Policy-Report-Only: default-src 'self' https://expected1.example.com https://expected2.example.com ; report-uri https://a-url-you-control
Why
This forms part of your technical audit, in an ongoing fashion. The initial technical audit you performed will give you a
list of third parties that your site shares or passes user data to. This header will then cause page requests to report
back which third parties are now being contacted, and you can track changes over time. This not only alerts you to changes
made by your existing third parties, but will also flag newly added third parties which weren't added to the technical audit.
It's important to update the header to stop reporting about domains you expect, but it's also important to repeat the manual
technical audit from time to time (because the Content-Security-Policy approach does not flag what data is passed, only
that a request has been made.)
Note that it does not need to be added to the pages served every time, or every page. Tune down how many page responses receive the header so that you receive a representative sample of reports that aren't overwhelming in quantity.
Permissions policy
The Permissions-Policy header (which was introduced under the name Feature-Policy) is similar in concept to Content-Security-Policy,
but it restricts access to powerful browser features. For example, it’s possible to restrict use of device hardware such as the accelerometer,
camera, or USB devices, or to restrict non-hardware features such as permission to go fullscreen or use synchronous XMLHTTPRequest.
These restrictions can be applied to a top-level page (to avoid loaded scripts from attempting to use these features) or to
subframed pages loaded in via an iframe. This restriction of API usage isn’t really about browser fingerprinting; it's about disallowing third-parties from doing intrusive things (such as using powerful APIs, popping up
permissions windows, etc). This is defined by the Target Privacy Threat Model as "intrusion".
A Permissions-Policy header is specified as a list of (feature, allowed origins) pairs, thus:
Permissions-Policy: geolocation=(self "https://example.com"), camera=(), fullscreen=*
This example allows this page ("self") and <iframe>s from the origin example.com to use the navigator.geolocation APIs
from JavaScript; it allows this page and all subframes to use the full screen API, and it prohibits any page, this page included,
from using a camera to read video information. There is much more detail and a list of potential examples here.
The list of features that are handled by the Permissions-Policy header is large and may be in flux. Currently the list is maintained at https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md.
{% Aside 'gotchas' %}
Note that Content-Security-Policy values are separated by semicolons but Permissions-Policy values are separated by commas,
and that Permissions-Policy values have brackets around them. The details are specified in the
Permissions Policy explainer.
{% endAside %}
Do
Browsers that support Permissions-Policy disallow powerful features in subframes by default, and you have to act to enable them!
This approach is private by default. If you find that subframes on your site require these permissions, you can selectively add them.
However, there is no permissions policy applied to the main page by default, and so scripts (including third-party scripts) that are
loaded by the main page are not restricted from attempting to use these features. For this reason, it is useful to apply a restrictive
Permissions-Policy to all pages by default, and then gradually add back in features that your pages require.
Examples of powerful features that Permissions-Policy affects include requesting the user's location, access to sensors (including
accelerometer, gyroscope, and magnetometer), permission to go fullscreen, and requesting access to the user’s camera and microphone.
The (changing) full list of features is linked above.
Unfortunately, blocking all features by default and then selectively re-allowing them requires listing all features in the header,
which can feel rather inelegant. Nonetheless, such a Permissions-Policy header is a good place to start. permissionspolicy.com/
has a conveniently clickable generator to create such a header: using it to create a header which disables all features results in this:
Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(),
display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(),
fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(),
payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
Understand built-in privacy features in web browsers
In addition to the "build time" and "design time" protections, there are also privacy protections which happen at "run time": that is, privacy features implemented in browsers themselves to protect users. These are not features you can directly control or tap into as a site developer—they're product features—but they are features you should be aware of, because your sites may be affected by these product decisions in browsers. To give an example here of the way these browser protections may impact your site, if you have client-side JavaScript which waits for a third-party dependency to load before continuing with page setup, and that third-party dependency never loads at all, then your page setup may never complete and so the user is presented with a half-loaded page.
They include Intelligent Tracking Prevention in Safari (and the underlying WebKit engine), and Enhanced Tracking Protection in Firefox (and its engine, Gecko). These all make it difficult for third parties to build detailed profiles of your users.
Browser approaches on privacy features change frequently, and it’s important to stay up to date; the following list of protections
are correct at time of writing. Browsers may also implement other protections; these lists are not exhaustive. See the module on best practices
for ways to keep up with changes here, and be sure to test with upcoming browser versions for changes that may affect your projects.
Bear in mind that incognito/private browsing modes sometimes implement different settings from the browser’s default (third-party cookies may be blocked
by default in such modes, for example). Therefore, testing in incognito mode may not always be reflective of most users’ typical browsing experience if
they're not using private browsing. Also bear in mind that blocking cookies in various situations may mean that other storage solutions, such as window.localStorage,
are also blocked, not only cookies.
Chrome
Edge
Firefox
To get some insight into what may be blocked and to help debug issues, click the shield icon in the address bar or visit about:protections in Firefox (on desktop).
Safari
(https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ summarizes these features.)
To get some insight into what may be blocked and to help debug issues, enable "Intelligent Tracking Prevention Debug Mode" in Safari's developer menu (on desktop). (See https://webkit.org/blog/9521/intelligent-tracking-prevention-2-3/ for more details.)
API proposals
Why do we need new APIs?
While new privacy-preserving features and policies in browser products help preserve user privacy, they also come with challenges. Many web technologies are usable for cross-site tracking despite being designed for and used for other purposes. For example, many identity federation systems we use every day rely on third-party cookies. Numerous advertising solutions that publishers rely on for revenue today are built on top of third-party cookies. Many fraud detection solutions rely on fingerprinting. What happens to these use cases when third-party cookies and fingerprinting go away?
It would be difficult and error-prone for browsers to differentiate use cases, and to reliably distinguish privacy-violating uses from others. This is why most major browsers have blocked third-party cookies and fingerprinting or intend to do so, to protect user privacy.
A new approach is needed: as third-party cookies are phased out and fingerprinting mitigated, developers need purpose-built APIs that meet the use cases (which haven’t gone away) but are designed in a privacy-preserving manner. The goal is to design and build APIs which can’t be used for cross-site tracking. Unlike the browser features described in the previous section, these technologies aspire to become cross-browser APIs.
Examples of API proposals
The following list isn’t comprehensive—it's a flavor of some of what’s being discussed.
Examples of API proposals to replace technologies built on third-party cookies:
- Identity use cases: FedCM
- Ads use cases: Private Click Measurement, Interoperable Private Attribution, Attribution Reporting, Topics, FLEDGE, PARAKEET.
Examples of API proposals to replace technologies built on passive tracking:
- Fraud detection use cases: Trust Tokens.
Examples of other proposals that other APIs can build on in a future without third-party cookies:
Additionally, another type of API proposal is emerging to try and have so-far browser-specific covert tracking mitigations. One example is Privacy Budget. Across these various use cases, the APIs that were initially proposed by Chrome live under the umbrella term of the Privacy Sandbox.
Global-Privacy-Control is a header that intends to communicate to a site that the user would like any collected personal data not to be shared with other sites. Its intention is similar to Do Not Track, which was discontinued.
{% Aside %} In the early 2000s there was a push from consumer advocacy groups to allow users to specify that they did not want to be tracked via cookies or similar technologies. All the major browsers implemented this as a request header, Do Not Track, with value 1 to indicate to websites that the user did not want to be tracked. This received a lot of pushback from data-collecting companies, and there was no requirement that it be obeyed and it largely was not. Towards the end of the 2010s it was largely dropped from browsers and the working group responsible closed, saying "there have not been indications of planned support among user agents, third parties, and the ecosystem at large". {% endAside %}
Status of the API proposals
Most of these API proposals are not yet deployed, or are deployed only behind flags or in limited environments for experimentation.
This public incubation phase is important: browser vendors and developers collect discussion and experience with whether these APIs are useful and whether they actually do what they’re designed for. Developers, browser vendors, and other ecosystem actors use this phase to iterate on the API's design.
The best place to keep up to date with new APIs being proposed is currently the Privacy Group’s list of proposals on Github.
Do you need to use these APIs?
If your product is directly built on top of third-party cookies or techniques such as fingerprinting, you should get involved with these new APIs, experiment with them, and contribute your own experiences to the discussions and API design. In all other cases, you don’t necessarily need to know all the details on these new APIs at the time of writing, but it’s good to be aware of what’s coming.
Learn PWA
Welcome to Learn Progressive Web Apps!
This course covers the fundamentals of Progressive Web App development in easy-to-understand pieces. Over the following modules, you'll learn what a Progressive Web App is, how to create one or upgrade your existing web content, and how to add all the pieces for an offline, installable app. Use the menu pane to navigate the modules. (The menu is at left on desktop or behind the hamburger menu on mobile.)
You'll learn PWA fundamentals like the Web App Manifest, service workers, how to design with an app in mind, how to use other tools to test and debug your PWA. After these fundamentals, you'll learn about integration with the platform and operating system, how to enhance your PWA's installation and usage experience, and how to offer an offline experience.
Each module includes interactive demos and self-assessments for you to test your knowledge. You will be able to test and try demos on your mobile phone, tablet, or laptop while playing with code to understand the fundamentals of what makes a Progressive Web App.
This course is created for a beginner and advanced web developers alike. You can go through the series from start to finish to get a general understanding of PWA from top to bottom, or you can use it as a reference for specific subjects. For those new to web development you will need a grounding in HTML, CSS, and JavaScript to follow along. Check out Learn CSS, and the HTML and JavaScript courses on MDN.
Here's what you'll learn:
{% include 'partials/course-index.njk' %}
So, are you ready to learn PWA?
{% Aside %} This course was written by Maximiliano Firtman, with input and review from Rachel Andrew, Adriana Jara, Pete LePage, Sam Richard, and Joe Medley. {% endAside %}
App design
This chapter focuses on some critical aspects of rendering content outside of the browser tab.
The window
Different operating systems have different ideas about what an application window is. For example, on iPhones, an application always takes up 100% of the screen. On Android and iPads applications usually run fullscreen but it is possible to share the screen between two apps, however there is only one app instance open at a time. In contrast, on a desktop device, an application can have more than one instance open at a time. It shares the available screen real estate with all other open applications. Each application instance can be resized and positioned anywhere on the screen, even overlapping other applications.
The icon
We recognize apps by their icon. That icon appears when you search for apps, in settings, wherever you launch apps, and where you see running apps.
These include:
- Home screen (iOS, iPadOS, Android).
- App Launcher (macOS, Android).
- Start Menu or App Menu (Windows, ChromeOS, Linux).
- Dock, TaskBar, or Multi-task panels (all operating systems).
When creating the icon for your Progressive Web App make sure its icon is platform-agnostic, as each operating system can render icons and apply different shape masks to them, like the ones in the image below.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/geo6MZwaWvNDRFJe6cvF.png", alt="PWA icons in different shapes for different platforms.", width="800", height="207" %}
Theming your app
There are a number of ways you can customize app styling in your PWA's, including:
- Theme color: defines the color of the window's title bar on the desktop and the color of the status bar on mobile devices. Using a meta tag, you can have options for different schemes, such as dark or light mode and they will be used based on the user's preference.
- Background color: defines the color of the window before the app and its CSS are loaded.
- Accent color: defines the color of built-in browser components, such as form controls.
When planning your Progressive Web App user interface, beware that you can't use images, or gradients for your theme, just plain colors. It's best to pick colors without transparency,
and use the rgb or hsl colors, hex codes, or named colors.
Theming your app may also affect browser theming even without installing the app, such as when using the <meta name="theme-color"> element.
{% endAside %}
Display modes
You can define what kind of window experience you'd like for your Progressive Web App. There are three options to choose from:
- Fullscreen
- Standalone
- Minimal User Interface
{% Aside %} If a particular display mode isn't supported or otherwise can't be used to display your PWA, it'll fall back to the next available display mode. Fullscreen will fall back to standalone, standalone will fall back to minimal user interface, and minimal user interface will fall back to browser, the default. The browser display mode doesn't show up as its own window, but rather displays your PWA in a tab in the browser it was installed from, like a web bookmark on a home screen. {% endAside %}
Fullscreen experience
A fullscreen experience is suitable for immersive experiences, such as games, VR, or AR experiences. It's currently only available on Android devices, and it hides the status bar and the navigation bar, giving your PWA 100% of the screen for your content.
On desktop and iPadOS, fullscreen PWAs are not supported; however, you can use the Fullscreen API from within your PWA to display your app fullscreen at a user's request.
Standalone experience
The most common option for a Progressive Web App, standalone mode displays your PWA in an OS-standard window without any browser navigation UI. The window may also include a browser-controlled menu where the user can:
- Copy the current URL.
- See, apply, or disable browser extensions.
- See and change permissions.
- Check current origin and the SSL certificate.
The title bar may also show permissions and hardware usage replacing the omnibox or URL bar when the PWA renders in the tab.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/2RmIdDEe4gm99lTzwLCa.png", alt="A PWA installed with Google Chrome on desktop showing the drop-down menu and the plugins-icon.", width="800", height="640" %}
On mobile devices, a standalone PWA experience will create a standard screen that keeps the status bar visible, so the user can still see notifications, time, and battery level. It often does not have any browser-controlled menu like desktop standalone experiences may include.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/sNa6aVY2ZayRqPZhpfIl.png", alt="An iOS device rendering a standalone app.", width="800", height="476" %}
Some browsers on Android create a fixed and silent notification while the PWA is in the foreground that lets the user copy the current URL or other options.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/iE1jwkZq1VWMJC8RZxJs.png", alt="An Android notification rendered by Chrome showing some actions over the current active PWA.", width="730", height="428" %}
Minimal user interface
This mode is available for Progressive Web Apps on Android and desktop operating systems. When you use it, the browser rendering your PWA will show a minimal user interface to help the user navigate within the application.
On Android, you will get a title bar that renders the current <title> element and the origin with a small drop-down menu available. On desktop, you will get a set of buttons in the title bar to help with navigation, including a back button and a control that swaps between a stop and a reload action, based on the current loading status.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/nzzjXh6YK5GzP6K2Sfb5.png", alt="A desktop minimal-ui on Microsoft Edge with back and reload buttons", width="800", height="399" %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/wu1S51rlQpgBcDkJgEvG.png", alt="On Android, browsers use a read-only themed navigation bar for minimal-ui, here Firefox and Chrome", width="800", height="249" %}
{% Aside 'warning' %} Safari on iOS and iPadOS doesn't support Minimal UI mode for PWAs. Instead, a browser shortcut is used as a fallback. The user can add an icon to the home screen, but that icon will point to Safari as a bookmark, and they won't have an app experience out of the browser tab. {% endAside %}
Optimizing design for desktop
When you design a Progressive Web App to work on a desktop, you need to think about the endless possibilities for window size as compared to being in the browser's tab or as an app in a mobile operating system.
In Chapter 3, we mentioned mini-mode: a desktop app can be as small as 200 by 100 pixels. This window will use the <title> element's content in your HTML as the title of the window. That content is also rendered when you alt-tab"** between apps and in other places.
Pay attention to your HTML's <title> element and rethink how you use it. The <title> isn't just for SEO or to render just the first characters in a browser's tab; it will be part of the user experience of your standalone desktop window.
{% Aside %}
If you are developing your app using a client-side library, such as React or Vue, don't forget to change the <title> to some practical value when moving between sections or routes of your app. It's time to reimagine the <title>!
{% endAside %}
CSS best practices
All your experience with CSS layout, design, and animation is valid when your content is rendered in its standalone experience. However, there are a couple of tips and tricks to making the experience better for a standalone window.
{% Aside 'gotchas' %} Remember that CSS only defines styles for the content of the PWA; later in this course, we will see how to specify style declarations for the window around your content, such as properties in the web app manifest. {% endAside %}
Media Queries
The first media query that you can take advantage of in your PWA is the display-mode property accepting the values browser, standalone, minimal-ui, or fullscreen.
This media query applies different styles to each mode. For example, you could render an installation invitation only when in browser mode, or render one particular piece of information only when the user uses your app from the system icon. This might include adding a back button to use when your app launches in standalone mode.
/* It targets only the app used within the browser */
@media (display-mode: browser) {
}
/* It targets only the app used with a system icon in standalone mode */
@media (display-mode: standalone) {
}
/* It targets only the app used with a system icon in all mode */
@media (display-mode: standalone), (display-mode: fullscreen), (display-mode: minimal-ui) {
}
{% Aside %}
Users expect apps to integrate with the platform, so consider offering dark and light modes using prefers-colors-scheme and honoring prefers-reduced-motion media queries to disable or reduce transitions and animations.
{% endAside %}
The app experience
When the users are using an installed PWA, they expect app behavior. While it's not simple to define what it means, the default web behavior doesn't give the best experience in some situations.
User selection
Content is generally selectable with a mouse or pointer, or a press and hold touch gesture. While helpful for content, it doesn't provide the best experience for navigation items, menus, and buttons within your PWA.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/F9HCkRrmPyGFjlpTJhNz.png", alt="A calculator PWA where you can select every interactive button, such as the numbers.", width="800", height="739" %}
Therefore, it's a good idea to disable user selection on these elements using user-select: none and it's -webkit- prefix version:
.unselectable {
user-select: none;
}
{% Aside 'gotchas' %} User selection is a vital feature of every web app for usability and accessibility. Do not disable user selection on all content, only on UI elements when not doing so creates a poor or unexpected experience. {% endAside %}
Accent color
In your PWA, you can define a color to match your brand within HTML form controls using the property accent-color.
System fonts
If you want an element, like dialogs or messages, to match the user's default platform font, you can use the following font family:
selector {
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", system-ui, Roboto, Oxygen-Sans, Ubuntu, Cantarell,
"Helvetica Neue", sans-serif;
}
Pull to refresh
Modern mobile browsers, such as Google Chrome and Safari, have a feature that refreshes the page when it is pulled down. On some browsers, such as Chrome on Android, that behavior is also enabled on standalone PWAs.
You may want to disable this action. For example when providing your own gesture management or refresh action, or if there is a possibility that your user will trigger the action unintentionally.
It's possible to disable this behavior by using overscroll-behavior-y: contain:.
body {
overscroll-behavior-y: contain;
}
Safe areas
Some devices do not have unobstructed rectangular screens; instead, their screen may be a different shape, like a circle, or have portions of the screen that can't be used, like the iPhone 13's notch. In these cases, some browsers will expose environment variables with safe areas that can display content.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/hKWqUXIbvvH05WHQEccm.png", alt="At top, a notch-based device in landscape with a standard viewport showing unrendered areas at the sides; at bottom, a device with full viewport access thanks to viewport-fit=cover.", width="800", height="1006" %}
If you want full access to the screen, even the invisible area, to render your color or image, include viewport-fit=cover in the content of your <meta name="viewport"> tag. Then use the safe-area-inset-* environment variables to extend your content safely into those areas.
{% Aside 'caution' %}
When you apply viewport-fit=cover, remember that you can now render pixels behind rounded corners and notches, so you should always use safe margins or paddings for the critical content and interactive elements.
{% endAside %}
Resources
- CSS display-mode media query
- Take control of your scroll: customizing pull-to-refresh and overflow effects
- prefers-color-scheme: Hello darkness, my old friend
- prefers-reduced-motion: Sometimes less movement is more
- env() CSS function
- CSS Accent Color
Architecture
Designing your application to make the most out of the technology that makes PWAs reliable, installable, and capable starts with understanding your application and its constraints, and choosing an appropriate architecture for both.
SPA versus MPA
Today, there are two primary architectural patterns in web development: single-page apps, or SPAs, and multi-page apps, or MPAs.
Single-page apps are defined by having client-side JavaScript control most or all of the HTML rendering of a page based on data retrieved by or provided to the app. The app overrides the browser's built-in navigation, replacing it with its routing and view handling functionality.
Multi-page apps usually have pre-rendered HTML sent directly to the browser, often enhanced with client-side JavaScript after the browser has finished loading the HTML, and relying on the browser's built-in navigation mechanisms to display subsequent views.
Both architectures can be used to create PWAs.
Each has advantages and disadvantages, and selecting the right one for your use case and context is key to providing a fast and reliable experience for your users.
Single page apps
{% Compare 'better', 'Pros' %}
- Mostly atomic in-page updates.
- Client-side dependencies loaded on start-up.
- Subsequent loads are fast, because of cache usage. {% endCompare %}
{% Compare 'worse', 'Cons' %}
- High initial load cost.
- Performance depends on device hardware and network connection.
- Additional app complexity is required. {% endCompare %}
Single page apps are a good architectural fit if:
- User interaction is mainly centered around atomic updates of interconnected data displayed on the same page, for instance, a real-time data dashboard or a video-editing app.
- Your application has client-side-only initialization dependencies, for instance, a third-party authentication provider with a prohibitively high startup cost.
- The data required for a view to load relies on a specific client-side-only context, for instance, displaying controls for a piece of connected hardware.
- The app is small and simple enough that its size and complexity do not have an impact on the cons listed above.
SPAs might not be a good architecture choice if:
- Initial load performance is essential. SPAs usually need to load more JavaScript to determine what to load and how to display it. The parsing and execution time of this JavaScript, combined with retrieving content, is slower than sending rendered HTML.
- Your app runs mostly on low-to-average-powered devices. Because SPAs depend on JavaScript for rendering, the user experience depends much more significantly on the power of their specific device than it would in an MPA.
Because SPAs need to replace the browser's built-in navigation with their routing, SPAs require a minimum level of complexity around efficiently updating the current view, managing navigation changes, and cleaning up previous views that would otherwise be handled by the browser, making them harder overall to maintain and more taxing on the user's device.
Multi-page apps
{% Compare 'better', 'Pros' %}
- Mostly full-page updates.
- Initial render speed is critical.
- Client-side scripting can be an enhancement. {% endCompare %}
{% Compare 'worse', 'Cons' %}
- Secondary views require another server call.
- Context doesn't carry over between views.
- Requires a server or pre-rendering. {% endCompare %}
Multi-page apps are a good architectural choice if:
- User interaction is mainly centered around views of a single piece of data with optional context-based data, for instance, a news or e-commerce app.
- Initial render speed is critical, as sending already rendered HTML to the browser is faster than assembling it from a data request after loading, parsing, and executing a JavaScript-based alternative.
- Client-side interactivity or context can be included as an enhancement after initial load, for instance, layering a profile onto a rendered page or adding secondary client-side context-dependent components.
MPAs might not be a good architecture choice if:
- Re-downloading, re-parsing, and re-executing your JavaScript or CSS is prohibitively expensive. This con is mitigated in PWAs with service workers.
- Client-side context, such as user location, doesn't seamlessly carry over between views, and re-obtaining that context may be expensive. It either needs to be captured and retrieved, or re-requested between views.
Because individual views need to be dynamically rendered by a server or pre-rendered before access, potentially limiting hosting or adding data complexity.
Which one to choose?
Even with these pros and cons, both architectures are valid for creating your PWA. You can even mix them for different parts of your app, depending on its needs, for instance, having store listings follow an MPA architecture and the checkout flow follow an SPA architecture.
Regardless of choice, the next step is understanding how to best use service workers to provide the best experience.
The power of service worker
The service worker has a lot of power beyond basic routing and delivery of cached and network responses. We can create complex algorithms that can improve the user's experience and performance.
Service worker includes (SWI)
An emerging pattern for using service workers as an integral part of a site's architecture is service worker includes (SWI). SWI divides individual assets, usually an HTML page, into pieces based on their caching needs, then stitches them back together in the service worker to improve consistency, performance, and reliability, while reducing cache size. {% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/xDjYv2TYJik4xm1h7nyS.png", alt="A website with a global header, a content area, a sidebar and a footer.", width="800", height="520" %}
This image is a sample web page. It has five different sections that break the page down into:
- Overall layout.
- Global header (top dark bar).
- Content area (middle left lines and image).
- Sidebar (tall medium-dark bar on the middle right).
- Footer (dark bottom bar).
Overall layout
The overall layout isn't likely to change often and has no dependencies. It's a good candidate for precaching.
Header and footer
The global header and footer contain things like the top menu and site footer, and present a particular challenge: if the page were to be cached as a whole, these might change between page loads, depending on when the given page was cached.
By separating them and caching them independently of the content, you can ensure that users will always get the same version, regardless of when they are cached. Because they are infrequently updated, they're good candidates for precaching, too. They have a dependency, though: the site's CSS and JavaScript.
CSS and JavaScript
Ideally, the site's CSS and JavaScript should be cached with a stale while revalidate strategy to allow incremental updates without needing to update the service worker, as it is the case with precached assets. Still, they also need to be kept at a minimum version whenever the service worker updates with a new global header or footer. Because of this, their cache should also be updated with the latest version of assets when the service worker installs.
Content area
Next is the content area. Depending on the frequency of updates, either network first or stale while revalidate is a good strategy here. Images should be cached with a cache first strategy, as has been previously discussed.
Sidebar
Finally, presuming the sidebar content contains secondary content such as tags and related items, it's not critical enough to pull from the network. A stale while revalidate strategy works for this.
Now, after going through all that, you may be thinking that you can only do this kind of per-section caching for single-page apps. But, by adopting patterns inspired by edge side includes or server side includes in your service worker, with some advanced service worker features, you can do this for either architecture.
Try it yourself
You can try the service worker includes with the next codelab:
{% Aside 'codelab' %} Service worker includes. {% endAside %}
Streaming responses
The previous page could be created using the app shell model in the SPA world, where the app shell is cached, then served, and content is loaded on the client side. With the introduction and wide availability of the Streams API, both app shell and content can be combined in the service worker and streamed to the browser, giving you the caching flexibility of app shell with the speed of MPAs.
It does this because:
- Streams can be built asynchronously, allowing different pieces of a stream to come from other sources.
- The requester of a stream can start working on the response as soon as the first chunk of data is available, instead of waiting for the entire item to be complete.
- Parsers optimized for streaming, including the browser, can progressively display the content of the stream before it's complete, speeding up the perceived performance of the response.
Thanks to these three properties of streams, architectures built around streaming usually have a faster perceived performance than those that aren't.
Working with the Streams API can be challenging as it's complex and low level. Fortunately, there's a Workbox module that can help with setting up streaming responses for your service workers.
Domains, origins, and PWA scope
Web workers, including service workers, storage, even an installed PWA's window, are all governed by one of the most critical security mechanisms on the web: the same-origin policy. Within the same origin, permissions are granted, data can be shared, and the service worker can talk to different clients. Outside of the same origin, permissions are not automatically granted and data is isolated and not accessible between different origins.
Same-origin policy
Two URLs are defined as having the exact origin if the protocol, port, and host are the same.
For example: https://squoosh.app, and https://squoosh.app/v2 have the same origin, but http://squoosh.app, https://squoosh.com, https://app.squoosh.app and https://squoosh.app:8080 are in different origins. Check the same-origin policy MDN reference for more information and examples.
Changing subdomains isn't the only way a host can change. Each host is made up of a top-level domain (TLD), a secondary level domain (SLD), and zero or more labels (sometimes called subdomains), separated by dots in between and read from right to left in an URL. A change in any of the items results in a different host.
In the window management module, we've already seen how the in-app browser looks when a user navigates to a different origin from an installed PWA.
That in-app browser will appear even if the websites have the same TLD and SLD, but with different labels, as they are then considered different origins.
One of the key aspects of an origin in a web-browsing context is how storage and permissions work. One origin shares many features among all content and PWAs within it, including:
- Storage quota and data (IndexedDB, cookies, web storage, cache storage).
- Service worker registrations.
- Permissions granted or denied (such as web push, geolocation, sensors).
- Web push registrations.
When you move from one origin to another, all the previous access is revoked, so permissions have to be granted again, and your PWA can't access all the data saved in the storage.
{% Aside 'caution' %}
Having different subdomains for different parts of a website or different TLDs for other locales, such as admin.example.com or example.co.uk, makes building PWAs harder, each subdomain will have isolated storage, its own service worker registration, and web app manifest. It may also lead to a disjointed experience if the subdomains are designed to look like the same app, as one will be displayed in the in-app browser while the main PWA won't.
{% endAside %}
{% Video src="video/RK2djpBgopg9kzCyJbUSjhEGmnw1/V1OWWZnCGpwVcaZE152Z.mp4", autoplay=true, controls=true, loop=true %}
Resources
- Beyond SPAs: Alternative architectures for your PWA
- Progressive web apps Structure
- Streams: The definitive guide
Assets and data
Developing an application typically involves several assets and resources, from the logic and code (compiled or not) to the user interface elements such as screen designs, images, logos, and fonts.
A Progressive Web App is a website, so all its assets are the same as on the web:
- HTML for content and initial page rendering.
- JavaScript for logic, including the ability to run WebAssembly modules (compiled code) and render 2D and 3D hardware-optimized graphics.
- CSS for layout, styling, and animations.
- Images in web formats, such as PNG, JPEG, WebP, and SVG.
- Videos in web formats, such as MPEG-4 and WebM.
- Web fonts.
- Data in JSON or other formats.
By default, websites download assets over the network, starting with HTML and continuing to the rest of the resources.
Managing those resources to load fast and be available offline has been a challenge for the web. Nowadays, PWAs use capabilities previously associated only with platform-specific apps.
Platform-specific apps vs. PWA
When you install a platform-specific app you are typically downloading a package that includes all the app's assets: code, images, fonts, videos, and so on. Therefore, all these resources are available, from your device storage, even when the app is offline.
On the other hand, a classic website needs a network connection to download the assets when required. If you are offline, you will see an error from the browser as there are no assets client-side.
The PWA approach enhances the traditional web experience by making some or all the assets available client-side as with platform-specific apps. Therefore, when you open a PWA, the initial rendering can be as instantaneous as a platform app because the assets are available without going to the network.
{% Aside %} An app that is available offline will always render a basic user interface instead of a technical error from the operating system or browser. Some apps still offer all their functionality, some a limited experience, and others a simple message with the app's branding. This applies to both platform-specific apps and PWAs. {% endAside %}
Caches and storage
Since the beginning of the web, developers have not had complete control of how a resource is cached. The browser is in charge of the HTTP cache and it may or may not cache and serve resources based on different policies. Other storage options like Web Storage and IndexedDB were meant to save simple data and objects.
PWAs don't need to rely on those policies alone for their content. Instead, we have solutions today to gain better control over when and how to cache resources and when and how to deliver them locally: the Cache Storage API. The web has a few of client-side storage solutions available:
- Web storage: Includes
localStorageandsessionStorage. These APIs store simple key/value string pairs. Web storage is limited, and has a synchronous API, so it should be avoided whenever possible. - IndexedDB: An object-based database (No-SQL) with an asynchronous API that is good for web performance. IndexedDB can store structured and binary data client-side. It is typically what you will use to store data, like what you'd get from or send as an API request. It's also useful to save data locally immediately and later on sync it to the server. The IndexedDB API is used to interact with the database.
- Cache storage: A collection of HTTP request and response pairs you can use to store and retrieve resources — with their HTTP headers — exactly as they are delivered from the server. The Cache Storage API allows you to store, retrieve, update and delete network requests, for example for your assets, even when offline.
{% Aside 'warning' %} You may have heard about WebSQL and ApplicationCache. These two solutions are deprecated, and you shouldn't use them in your PWA. {% endAside %}
The need for service workers
A PWA can store its assets in Cache Storage and IndexedDB, but how can we use those assets to deliver a fast and offline experience to your users. The answer? Service workers.
With a service worker, you can serve assets without going to the network, send notifications to a user, add a badge to your PWA icon, update content in the background, and even make your whole PWA work offline. Learn more about service workers in the next chapter.
Offline-ready
Users expect your application to offer a fast and always-ready experience. That means your app should work offline.
Being offline-ready doesn't mean that all your content or services should be available without a network connection. However having at least a basic experience when the user is offline, like a page that asks you to connect to the Internet to continue, will provide for a better user experience, keeping the user within the experience of your app instead of a generic browser error. In some browsers this is a must-have feature to pass the PWA installation criteria. Displaying your PWA's user interface, along with cached content, is better. Letting users continue using your whole PWA and syncing server changes when they're back online is the gold standard for working offline.
To make your app available offline, you will need to cache the assets necessary for your offline experience and make your service worker serve them later. Make sure to add the offline assets to your cache before you need them. This is a particular case where you cannot cache them when requested.
{% Aside %} Asset caching happens independently from PWA installation, and will work even from a browser tab, allowing for faster asset delivery and an offline experience regardless of how a user chooses to use your PWA. {% endAside %}
Frequently used cache approaches
In your PWA, you are in charge of all the decisions. You can choose the best approach to cache or install assets based on your needs. Some decisions to make are:
- Precaching: pick the assets you'd like to install on the first load; those should include the HTML and the minimal assets to render the app. When using precache keep in mind you are using the device's space and network. Be conscious and responsible when downloading assets and caching them.
- Cache as needed: used to add assets to the cache when they are requested. Typically these are assets that can change independently of your app version, like images or content. See the caching section for different strategies on how to cache as needed.
- APIs and web services: API calls can be cached; or, instead of caching the API responses, you can store their data in IndexedDB.
{% Aside 'gotchas' %} With platform-specific apps, the most common pattern is to include all the app's assets in its package; that's why these apps are commonly larger than 500 MB. On the other hand, A PWA can initially install only the minimum set of resources to offer an offline and fast experience and download the rest later, with PWAs using considerably less storage on installation. {% endAside %}
Updating assets
In the standard app model for app catalog installed apps, when developers need to update their app, they publish a new package. Users need to download the whole package again, even if most of the assets haven't changed. With PWAs, using the approaches on the section above, you decide how and when to update assets. Here are different options for how to update assets:
- Full Update: in this case, every time you make a change in the app, even a minor one, your code will download all the assets again in the cache.
- Partial Update: in this approach, you create a method to define which assets have been updated, and you update only those, with or without the user's intervention.
- Continuous Update: using this technique, your assets will be checked and updated automatically on every PWA usage individually
{% Aside %} When caching assets, you need to decide on a cache swap or clean up policy that defines when the user will see an updated asset; taking into account storage usage and the balance between a fast experience and displaying a slightly stale asset. {% endAside %}
Size and Lifespan
Usually platform-specific apps don't deal with size limits; at installation, apps can be gigabytes or more in size. As long as the device has the capacity the installation will be allowed. Also, while the app is installed, it will use that total amount of storage no matter if you use the app or not. Storage is handled differently for PWAs. The browser will store your assets based on policies that you define in your PWA's logic.
{% Aside %} The users are always in control, and they can delete the assets at any time. For PWAs, manual eviction happens via the browser's settings. {% endAside %}
Size limits depend on the browser. It's not as flexible as for platform-specific apps, but it's typically good enough for most web apps. You can find the specific limitations by browser in this article.
Browsers can evict assets based on storage pressure, or after some weeks of inactivity, if the user is using your PWA in the browser. On some platforms, if the user installs your PWA, eviction won't happen. Where available, code can request persistent storage through an API to avoid that eviction.
{% Aside %} If storage eviction happens for any reason and the user installed your PWA, the PWA will still work if you have a network connection. When you open that app again, the browser will start the service worker lifecycle again, loading your app from the network. {% endAside %}
Resources
Caching
Cache storage is a powerful tool. It makes your apps less dependent on network conditions. With good use of caches you can make your web app available offline and serve your assets as fast as possible in any network condition. As mentioned in Assets and Data you can decide the best strategy for caching the necessary assets. To manage the cache your service worker interacts with the Cache Storage API. {% BrowserCompat 'api.CacheStorage' %}
{% Aside %} When installing platform-specific apps, the device stores the icon and other app assets in the operating system, in one step. For PWAs, the process has two separate steps. A PWA can store assets on your device any time after the first visit in the browser, even without installation. Install is a separate action that is covered later in this course. {% endAside %}
The Cache Storage API is available from different contexts:
- The window context (your PWA's main thread).
- The service worker.
- Any other workers you use.
One advantage of managing your cache using service workers is that its lifecycle is not tied to the window, which means you are not blocking the main thread. Be aware that to use the Cache Storage API most of these contexts have to be under a TLS connection.
What to cache
The first question you may have about caching is what to cache. While there is no single answer to that question, you can start with all the minimum resources that you need to render the user interface.
Those resources should include:
- The main page HTML (your app's start_url).
- CSS stylesheets needed for the main user interface.
- Images used in the user interface.
- JavaScript files required to render the user interface.
- Data, such as a JSON file, required to render a basic experience.
- Web fonts.
- On a multi-page application, other HTML documents that you want to serve fast or while offline.
{% Aside 'warning' %} Remember that you are downloading and storing assets on users' devices, so use that space and bandwidth responsibly. You need to find the balance between having enough on-device assets to render a fast or offline experience without consuming too much data. {% endAside %}
Offline-ready
While being offline-capable is one of the requirements for a Progressive Web App, it's essential to understand that not every PWA needs a full offline experience, for example cloud gaming solutions or crypto assets apps. Therefore, it's OK to offer a basic user interface guiding your users through those situations.
Your PWA should not render a browser's error message saying that the web rendering engine couldn't load the page. Instead use your service worker to show your own messaging, avoiding a generic and confusing browser error.
{% Aside 'caution' %} If you publish your PWA to Google Play Store, your PWA should never render an HTTP error message from the browser to avoid penalizations within the store listings. Check Changes to Quality Criteria for PWAs for more information. {% endAside %}
There are many different caching strategies you could use depending on the needs of your PWA. That's why it is important to design your cache usage to provide a fast and reliable experience. For example if all your app assets will download fast, don't consume a lot of space, and don't need to be updated in every request, caching all your assets would be a valid strategy. If on the other hand you have resources that need to be the latest version you might want to consider not caching those assets at all.
{% Aside 'caution' %} The cache storage content and eviction rules are set per origin and not per PWA, since it's possible to have more than one in a single origin. If you share your origin for many PWAs, it's a good idea to add a prefix to your cache names to avoid collision problems between each PWA's data storage. {% endAside %}
Using the API
Use the Cache Storage API to define a set of caches within your origin, each identified with a string name you can define. Access the API through the caches object, and the open method enables the creation, or opening of an already created cache. The open method returns a promise for the cache object.
caches.open("pwa-assets")
.then(cache => {
// you can download and store, delete or update resources with cache arguments
});
Downloading and storing assets
To ask the browser to download and store the assets use the add or addAll methods. The add method makes a request and stores one HTTP response, and addAll a group of HTTP responses as a transaction based on an array of requests or URLs.
caches.open("pwa-assets")
.then(cache => {
cache.add("styles.css"); // it stores only one resource
cache.addAll(["styles.css", "app.js"]); // it stores two resources
});
{% Aside %}
Both add() and addAll() return a promise with no arguments; if it's fulfilled, you know the assets were downloaded and cached, and if it fails, the API couldn't download one or more resources, and it didn't modify the cache.
{% endAside %}
The cache storage interface stores the entirety of a response including all the headers and the body. Consequently, you can retrieve it later using an HTTP request or a URL as a key. You will see how to do that in the Serving chapter.
{% Aside %} To download and store the assets, you must specify all the URLs explicitly. Otherwise the API cannot know all the assets you need or want to cache. {% endAside %}
When to cache
In your PWA, you are in charge of deciding when to cache files. While one approach is to store as many assets as possible when the service worker is installed, it is usually not the best idea. Caching unnecessary resources wastes bandwidth and storage space and could cause your app to serve unintended outdated resources.
You don't need to cache all the assets at once, you can cache assets many times during the lifecycle of your PWA, such as:
- On installation of the service worker.
- After the first page load.
- When the user navigates to a section or route.
- When the network is idle.
You can request caching new files in the main thread or within the service worker context.
Caching assets in a service worker
One of the most common scenarios is to cache a minimum set of assets when the service worker is installed. To do that, you can use the cache storage interface within the install event in the service worker.
Because the service worker thread can be stopped at any time, you can request the browser to wait for the addAll promise to finish to increase the opportunity of storing all the assets and keeping the app consistent. The following example demonstrates how to do this, using the waitUntil method of the event argument received in the service worker event listener.
const urlsToCache = ["/", "app.js", "styles.css", "logo.svg"];
self.addEventListener("install", event => {
event.waitUntil(
caches.open("pwa-assets")
.then(cache => {
return cache.addAll(urlsToCache);
});
);
});
The waitUntil() method receives a promise and asks the browser to wait for the task in the promise to resolve (fulfilled or failed) before terminating the service worker process. You may need to chain promises and return the add() or addAll() calls so that a single result gets to the waitUntil() method.
You can also handle promises using the async/await syntax. In that case, waitUntil() needs a promise-based function as an argument, so you need to create a function that returns the promise to make it work, as in the following example:
const urlsToCache = ["/", "app.js", "styles.css", "logo.svg"];
self.addEventListener("install", (event) => {
event.waitUntil(async () => {
const cache = await caches.open("pwa-assets");
return cache.addAll(urlsToCache);
});
});
Cross-domain requests and opaque responses
Your PWA can download and cache assets from your origin and cross-domains, such as content from third-party CDNs. With a cross-domain app, the cache interaction is very similar to same-origin requests. The request is executed and a copy of the response is stored in your cache. As with other cached assets it is only available to be used in your app's origin.
The asset will be stored as an opaque response, which means your code won't be able to see or modify the contents or headers of that response. Also, opaque responses don't expose their actual size in the storage API, affecting quotas. Some browsers expose large sizes, such as 7Mb no matter if the file is just 1Kb.
{% Aside 'caution' %}
Remember that when you cache opaque responses from cross-domains, cache.add() and cache.addAll() will fail if those responses don't return with a 2xx status code. Therefore, if one CDN or cross-domain fails, all the assets you are downloading will be discarded, even successful downloads in the same operation.
{% endAside %}
Updating and deleting assets
You can update assets using cache.put(request, response) and delete assets with delete(request).
{% Aside 'caution' %} The Cache Storage API doesn't update your assets if you change them on your server nor does it delete them. Your code should manage both situations, and for that, there are different design patterns. You'll learn about a library to help with these situations in the Workbox chapter. {% endAside %}
Check the Cache object documentation for more details.
{% Glitch 'learn-pwa-asset-caching' %}
Debugging Cache Storage
Many browsers offer a way to debug the contents of cache storage within their DevTools Application tab. There, you can see the contents of every cache within the current origin. We'll cover more about these tools in the Tools and Debug chapter.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/wY3mk9ILrGxlM9xEbfHh.png", alt="Chrome DevTools debugging Cache Storage contents.", width="800", height="506" %}
{% Aside %} Web Inspector on Safari on macOS doesn't have a way to see the contents of cache storage. For that purpose, you can use the free Service Worker Detector Safari extension created by Thomas Steiner. {% endAside %}
Resources
Capabilities
Progressive Web Apps can do more than just rendering content on the screen or connecting to web services. PWAs can deal with files from the file system, they can interact with the system's clipboard, they can access hardware that is connected to the device, and much more. Every Web API is available for your PWA, and there are some extra APIs available when your app is installed.
You can use What Web Can Do Today to know what's possible on each platform. For individual APIs or capabilities, you can use Can I Use or the browser compatibility tables on MDN.
Always check for feature support
The first letter in PWA stands for progressive, and it comes from the idea of progressive enhancement and feature detection. You should not expect your app to work the same way on every device. The diversity of contexts and abilities on billions of devices in different countries makes PWAs an excellent platform, thanks to their progressiveness.
This means that you need to develop your app in layers that may not be available on every device and to check availability before usage.
You need to check with JavaScript if an API exists before using it or ask an API if a service is available on that particular device.
Powerful web
The web is super powerful today. For example:
- You can build a hyperlocal video chat app with WebRTC, geolocation, and push messages.
- You can make an app installable.
- You can add video effects with WebAssembly.
- You can even bring it into virtual reality with WebGL and WebXR.
{% Aside 'gotchas' %} While there are many capabilities on the web, there are still gaps, and that's where the web capabilities project comes in. The project describes a group of APIs to increase what the web is capable of. We discussed this project and how you can participate in it in the Experimental chapter. {% endAside %}
Empowering your PWA
Let's split the PWA capabilities APIs into four groups:
- Green: APIs available on every browser on every platform, when technically possible. Most of them have been shipped for many years, they are considered mature, and you can use them with confidence. An example API from this group is the geolocation API.
- Light green: APIs are available only on some browsers. Considering the lack of support on some platforms, they matured within the supported subgroup of browsers so that you can use the capability confidently on them. An example API from this group is WebUSB.
- Yellow: experimental APIs. These APIs are not yet mature; they are only available on some browsers, and within tests or trials. We talked some about these capabilities in the Experimental chapter.
- Red: the group for APIs that you can't use in a PWA, and where plans to add them are still long term. An example API from this group is geofencing.
{% Aside %} Some capabilities require user permission: in most cases, the permission dialog appears on first usage. Today, you can request a single permission at a time, but in the future, it may be possible to request many permissions in one dialog on some platforms, using the Permissions API. {% endAside %}
Green capabilities
Below is a list of the most important capabilities you can use in your PWA.
Basics
- Caching files locally using the Cache API, as seen in the Caching chapter.
- Executing tasks in threads using web workers, as we saw in the Complexity management chapter.
- Managing network requests with different strategies in a service worker, as seen in the Service workers chapter.
- 2D Canvas for rendering 2D graphics on the screen using the Canvas API.
- 2D and 3D high-performance Canvas, or WebGL, for rendering 3D graphics.
- WebAssembly, or WASM, for executing low-level compiled code for performance.
- Real-Time communication, using the WebRTC API.
- Web Performance APIs to measure and help provide a better experience. See the Performance API guide for more information.
- Store data locally with IndexedDB and storage management to query quota and request persistent storage, as seen in the Offline data chapter.
- Low-level audio thanks to the Web Audio API.
- Foreground detection using the Page Visibility API.
- Network communication using the Fetch API and the WebSocket API.
Hardware and sensors
- Geolocation obtains the user's location through different providers, such as satellite or Wi-Fi through the Geolocation API.
- Camera and microphone receive media streams from cameras and microphones using the Media devices interface.
- Sensors gather real-time information from the accelerometer, gyroscope, magnetometer, and others using the Sensors API or older interfaces, such as DeviceMotionEvent and DeviceOrientationEvent. On Safari, you need to use a non-standard permission dialog request to use them.
- Touch and pointer access information about all the touches and pointer-based clicks you make with your finger, a mouse, a trackpad, or a pen, thanks to Pointer events and Touch events.
{% Aside %} Safari also supports Gesture events, a non-standard API for detecting rotation and pinching gestures, which should be used with care. {% endAside %}
- Gamepads to read information coming from standard gamepads and joysticks connected to the device using the Gamepad API.
- Biometric authentication (such as face or fingerprint recognition) using Web Authentication or WebAuthn.
Operating system integration
- Speech synthesis and voice recognition use the platform's installed voices to speak to the user and to recognize what the user is saying, thanks to the Web Speech API.
- Share content from your PWA to other apps and places on the device, thanks to the Web Share API, as we'll see in the OS integration chapter.
- Access the clipboard to save and retrieve content from the clipboard in different formats, thanks to the Clipboard API, as I showed in the OS integration chapter.
- Manage user's credentials and passwords using the Credential Management API.
- Enable picture-in-picture video playing within the OS using the picture-in-picture API.
- Render content in fullscreen thanks to the Fullscreen API, as I showed in the Windows chapter.
{% Aside %} You can integrate your PWA with many platform-specific apps using URI schemes and universal URLs, as I showed in the OS Integration chapter. {% endAside %}
Light-green capabilities
Here is a list of the most important capabilities you can use in your PWA, with the caveat that they may not be available on every browser.
The basics
- WebGL 2.0, the new version of the WebGL spec to match OpenGL 3.0.
- Codecs, low-level access to the individual frames of a video stream and chunks of audio; useful for web applications that require full control over the way media is processed through the Web Codecs API.
Hardware and sensors
-
Advanced camera controls to access pan, tilt, and zoom controls, in addition to the Media APIs.
-
Bluetooth LE to communicate with Bluetooth low-energy devices near the user using the Web Bluetooth API. Check out communicating with Bluetooth devices over JavaScript for more information.
-
Near-field communication to exchange data over NFC via light-weight NFC data exchange format (NDEF) messages, such as an NFC tag or card using the WebNFC API. You can also read Interact with NFC devices on Chrome for Android for more details.
-
Serial peripheral for low-level access to devices connected to the device using a serial port, USB, or serial over Bluetooth using the Web Serial WPI. In the following link you can learn how to read from and write to a serial port.
-
USB device access to communicate with devices connected via USB WebUSB API. Check out access to USB devices on the web for more information.
-
Human interface devices let your PWA interact with any kind of device prepared for human interaction that is uncommon, using the WebHID API. Check out this guide on connecting to uncommon HID devices.
{% Aside %} Accessing hardware devices on the web offers a comprehensive guide to understanding how different hardware APIs interact with each other. {% endAside %}
- Ambient Light reads the current light level or illuminance of the ambient light around the device, in addition to the Sensors API.
- Vibration gives the user haptic feedback when something happens, if the device supports it, through the Vibration API.
- Recording media captures the data generated by a MediaStream or HTMLMediaElement object (such as a
<video>tag) for analysis, processing, or saving to disk, thanks to the MediaRecorder API. - Applying a wake Lock to the screen prevents the device from dimming, or locking the screen, when your PWA needs to keep running, using the Screen Wake Lock API.
- Virtual reality enables you to use a headset and other devices in your PWA, thanks to the WebXR Device API.
- Augmented reality can be achieved in your PWA in many ways, such as using the WebXR Device API or the Safari Quick Look app for AR content.
- Detect inactive users with the Idle Detection API.
- Orientation lock locks the orientation to portrait or landscape while the PWA is on the screen, thanks to the Screen Orientation API, or the
orientationproperty of the Web App Manifest for installed apps. - Present content on projectors and secondary displays, thanks to the Presentation API.
- Lock a pointer to receive delta movement information from pointers (mice, trackpads, and pointers) instead of position values—useful for some games—thanks to the Pointer Lock API.
Operating system integration
- Read and write files on the device, thanks to the File System Access API, as you saw in the OS Integration chapter.
- Get content from other apps thanks to Web Share Target, as I showed in the OS integration chapter.
- Get contact data using the Contact Picker API, as shown in the OS integration chapter.
- Sync in the background while the PWA is not used, thanks to the Background Synchronization API.
- Task scheduling while the PWA is not in use, thanks to the Web Periodic Background Synchronization API.
- Send notifications using Web Push and Web Notifications APIs.
- Transfer files in the background while the user is not using your PWA, thanks to the Background Fetch API.
- Integrate your media playing with the operating system using the Media Session API.
- Manage payments in your PWA, thanks to the Payment Request API. Apple also offers an Apple Pay JS library on top of the Payment Request API.
- Query network status, such as connection type (4G, WiFi) and a Save Data flag using the Network Information API.
- Capture the user's screen for screencast or screen sharing using the Screen Capture API.
- Detect shapes using on-device hardware-accelerated detectors, including barcodes (human faces and text OCR are still in development) using the Shape Detection API.
- Query a device's memory using the Device Memory interface.
- One-time passwords over SMS let you automatically receive a code via SMS sent from your server using the WebOTP API. Safari implements a solution subset based on the
<input>element. Read more about it in WebKit's blog. - Manage the virtual keyboard that appears on mobile devices screens using the Virtual Keyboard API.
{% Aside %} If you publish a PWA to some app catalogs and stores, you may access additional APIs. For example, if you publish a PWA to Google Play using a Trusted Web Activity, you can use the Digital Goods API to charge your users for subscriptions and premium content. {% endAside %}
{% Aside 'gotchas' %} If you have a legitimate use case that cannot be realized with the present set of APIs, you can submit a request to analyze the use case for a possible future web API, as we'll see in the Experimental chapter. {% endAside %}
Resources
- MDN: Web APIs
- What Web Can Do Today
- What PWA Can Do Today
- Can I Use
- iOS/iPadOS Approximate location
- Meet Face ID and Touch ID for the Web
Complexity management
In his talk Simple Made Easy, Rich Hickey discusses the qualities of simple versus complex things. He describes simple things as focusing on:
"One role, one task, one concept, or one dimension."
But emphasizes that simplicity isn't about:
"Having one instance or performing one operation."
Whether or not something is simple is about how interconnected it is.
Complexity comes from binding, weaving, or, to use Rich's term, complecting things together. You can calculate complexity by counting the number of roles, tasks, concepts, or dimensions something manages.
Simplicity is essential in software development because simple code is easier to understand and maintain. Simplicity is also necessary for web apps because it may help to make our app fast and accessible in every possible context.
Managing PWA complexity
All JavaScript we write for the web touches the main thread at one point. The main thread, though, comes with a lot of complexity out-of-the-box that you, as a developer, have no control over.
The main thread is:
- Responsible for drawing the page, which itself is a complex, multi-step process involving calculating styles, updating and compositing layers, and painting to screen.
- Responsible for listening to and reacting to events, including events such as scrolling.
- Responsible for loading and unloading the page.
- Managing media, security, and identity. That's all before any code you write can even execute on that thread, such as:
- Manipulating the DOM.
- Accessing sensitive APIs, for example, device capabilities, or media/security/identity.
As Surma put it in his 2019 Chrome Dev Summit talk, the main thread is overworked and underpaid.
And yet, most application code lives on the main thread too.
All that code adds to the complexity of the main thread. The main thread is the only one that the browser can use to lay out and render content on the screen. Therefore, when your code requires more and more processing power to complete, we need to run it quickly, because every second it takes to perform application logic is a second the browser can't respond to user input or redraw the page.
When interactions don't connect to input, when frames drop, or when it takes too long to use a site, users get frustrated, they feel the application is broken, and their confidence in it decreases.
The bad news? Adding complexity to the main thread is an almost sure-fire way to make meeting these goals difficult. The good news? Because what the main thread needs to do is clear: it can be used as a guide to help reduce reliance on it for the rest of your application.
Separations of concerns
There are lots of different kinds of work that web applications do, but broadly speaking, you can break it down into work that directly touches the UI and work that doesn't. UI work is work that:
- Directly touches the DOM.
- Uses APIs that touch device capabilities, for example, notifications or file system access.
- Touches identity, for example, user cookies, local, or session storage.
- Manages media, for example, images, audio, or video.
- Has security implications that would require user intervention to approve, such as the web serial API.
Non-UI work can include things such as:
- Pure calculations.
- Data access (fetch, IndexedDB, etc.).
- Crypto.
- Messaging.
- Blob or stream creation, or manipulation.
Non-UI work is often bookended by UI work: a user clicks a button that triggers a network request for an API that returns parsed results that are then used to update the DOM. When writing code, this end-to-end experience is often considered, but where each part of that flow lives usually is not. The boundaries between UI work and non-UI work are as important to consider as the end-to-end experiences, as they are the first place you can reduce main thread complexity.
Focus on a single task
One of the most straightforward ways of simplifying code is breaking out functions so each focuses on a single task. Tasks can be determined by the boundaries identified by walking through the end-to-end experience:
- First, respond to user input. This is UI work.
- Next, make an API request. This is non-UI work.
- Next, parse the API request. This again is non-UI work.
- Next, determine changes to the DOM. This may be UI work, or if you're using something such as a virtual DOM implementation, it may not be UI work.
- Finally, make changes to the DOM. This is UI work.
The first clear boundaries are between UI work and non-UI work. Then there are judgment calls that need to be made: is making and parsing an API request one task or two? If DOM changes are non-UI work, are they bundled with the API work? In the same thread? In a different thread? The proper level of separation here is key to both simplifying your codebase and being able to successfully move pieces of it off the main thread.
Composability
To break up large end-to-end workflows into smaller parts, you need to think about your codebase's composability. Taking cues from functional programming, consider:
- Categorizing the types of work your application does.
- Building common input and output interfaces for them.
For instance, all API retrieval tasks take in the API endpoint and return an array of standard objects, and all data-processing functions take in and return an array of standard objects.
JavaScript has a structured clone algorithm meant for copying complex JavaScript objects. Web workers use it when sending messages and IndexedDB uses it to store objects. Choosing interfaces that you can use with the structured cloning algorithm will make them even more flexible to run.
With this in mind, you can create a library of composable functionality by categorizing your code and creating common I/O interfaces for those categories. Composable code is a hallmark of simple codebases: loosely coupled, interchangeable pieces that can sit “next” to each other and build on each other, as opposed to complex code that is deeply interconnected and therefore cannot be easily separated. And on the web, composable code can mean the difference between overworking the main thread or not.
With composable code in hand, it's time to get some of it off the main thread.
Using web workers to reduce complexity
Web workers, an often underutilized but widely available web capability, let you move work off the main thread.
Web workers let a PWA run (some) JavaScript outside of the main thread.
There are three kinds of workers.
Dedicated workers, what's most commonly thought of when describing web workers, can be used by a single script in a single running instance of a PWA. Whenever possible, work that doesn't directly interact with the DOM should be moved to a web worker to improve performance.
The final web worker is one covered extensively in this course: service workers, which act as a proxy for network requests and are shared between all instances of a PWA.
Try it yourself
It's code time! Build a PWA from scratch based on everything you've learned in this module.
{% Aside 'codelab' %} Progressive Web Apps: Working with Workers {% endAside %}
Resources
- Simple Made Easy (video)
- Using Web Workers
- Workers overview
- Use web workers to run JavaScript off the browser's main thread
Conclusion
Congratulations! You've made it to the end! We hope that you have enjoyed this course of Progressive Web Apps. In case you are using a library or a framework to create your web apps, you can check out the following additional resources:
Resources
- React with CRA: Making a PWA
- Next.js: PWA Sample
- Vue: CLI PWA Plugin
- Angular: Service Worker and PWA Documentation
- Svelte: Build a PWA
- Nuxt.js: PWA
- Jekyll PWA Plugin
- Gatsby: PWA
- ASP.NET: Build PWAs with ASP.NET Core Blazor WebAssembly
- Django: Make PWA of a Django Project
- Flutter: Building a web application with Flutter
- Wordpress: PWAs for Wordpress
Brought to you by
{% include 'partials/authors-stack.njk' %}
Detection
You can detect if the user is using your PWA in the browser or in standalone mode. On Chromium-based browsers (Android and desktop), you can also detect the following events:
- Installation invitation dialog status and result.
- Installation finished.
- Navigation transfer from browser to PWA window and conversely.
- PWA installation status.
- Related app installed from an app store.
You can use this data for analytics, to understand your user's preferences, and to customize their experience. To capture these events, you can use tools such as media queries, events from the window, or using the capabilities APIs that you can find listed here.
Detecting display mode
To track how users launch your PWA, you can use matchMedia() to test the display-mode media query.
window.addEventListener('DOMContentLoaded', () => {
let displayMode = 'browser tab';
if (window.matchMedia('(display-mode: standalone)').matches) {
displayMode = 'standalone';
}
// Log launch display mode to analytics
console.log('DISPLAY_MODE_LAUNCH:', displayMode);
});
If you use this example, remember to match the display mode from your web app manifest, for instance, standalone, minimal-ui or fullscreen. You can also match multiple queries in the media query string using comma-separated conditions.
You can also add a query parameter to your manifest's start_url that you can capture with analytics to track stats about when, in what way, and how much your PWA is being used.
{% Aside 'caution' %}
If you use a query parameter on your start_url to capture metrics, make sure to set the id field on your manifest to uniquely identify your app. Read how to set up this field in Uniquely identifying PWAs with the web app manifest id property
{% endAside %}
App installation
When a user accepts the install prompt in the browser, the appinstalled event fires on Chromium-based browsers. A great use for this event handler is to remove any in-app installation promotion you've added.
window.addEventListener('appinstalled', () => {
// If visible, hide the install promotion
hideInAppInstallPromotion();
// Log install to analytics
console.log('INSTALL: Success');
});
Remember, on Android devices with WebAPK, the event is fired when the user accepts the dialog, and not after the WebAPK is minted and installed. There may be a delay of some seconds before the app is fully installed.
The Installation Prompt chapter explains how to detect if the installation prompt is available and what choice the user makes.
{% Aside 'warning' %}
The appinstalled event is available only in Chromium-based browsers, and it's part of an incubator for the manifest spec
{% endAside %}
Session transfer
Users can use your PWA within the browser and in the installed standalone form. On desktop browsers, you can transfer the current navigation between these contexts using badges or menu items, as seen in the following image.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/260YbAY9W4qFitqUHhQ5.png", alt="Navigation transfer between a browser tab and a PWA window.", width="800", height="321" %}
On Android, there is a menu item similar to the one on desktop from the browser that transfers the navigation to the app. In this case, the current URL is opened but it will be a new page navigation in the app.
In the following image you can see the menu item in Android when the app is already installed.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/rpMHjK1uhwwJONJKTj2r.png", alt="Chrome on Android shows menu item to open a new navigation in a PWA window.", width="800", height="1689" %}
Transfer after installation
From desktop browsers, the current navigation is immediately transferred to the app on installation. The browser's tab is closed, and the newly installed app is opened, continuing what the user was doing.
On mobile browsers, your current navigation stays in the browser when you install the app. If the users want to move to the app, they need to open the app manually, and it will be a new navigation.
Detecting the transfer
To detect the transfer between browser and window, you can use a media query:
window.addEventListener('DOMContentLoaded', () => {
// replace standalone with your display used in manifest
window.matchMedia('(display-mode: standalone)')
.addListener(event => {
if (event.matches) {
// From browser to standalone
} else {
// From standalone to browser
}
});
});
iOS and iPadOS storage isolation
On iOS and iPadOS, there is no navigation or URL transfer between the browser and the installed icon. Even if it's the same PWA, every PWA icon that the user installs will have its own storage, isolated from Safari's tab and other icons. When a user opens the installed icon, no storage is shared with Safari. If your PWA needs a login, the user will need to log in again. If the app was added to the home screen several times, for each PWA instance, the user has a different session.
Related applications
{% BrowserCompat 'html.manifest.related_applications' %}
A website indicates a relationship with an app via the manifest. To do so, use the related_applications member of the Web App Manifest spec.
The related_applications key is an array of objects that represent each related application. Each entry contains platform, url, and an optional id.
These are the possible platform values:
play: Google Play apps (Android and ChromeOS).itunes: Apple App Store (macOS, iOS, and iPadOS).windows: Microsoft Store (Windows 10 and 11).amazon: Amazon AppStore (FireOS).webapp: installed PWA.
Prefer a related app
When the user installs an app, you can redirect the user to a related app if you set the prefer_related_applications field to true in your manifest. With this setup, on compatible browsers the install flows won't install the PWA; instead they trigger a store installation from the url or id that you specified in the related_applications entry.
{
...
"related_applications:" [
{
"platform": "play",
"url": "https://play.google.com/..."
}
],
"prefer_related_applications": true
}
{% Aside %}
Using prefer_related_applications: true with only a webapp preferred application doesn't make any difference in how your PWA is offered and installed.
{% endAside %}
Apple Smart App Banners
Safari doesn't support the related_applications member, but it offers Smart App Banners for apps in the App Store. So if you want to promote a PWA or other app you have published in the App Store, you can include meta tags in your PWA's HTML to invite the user to install the app (see the link just provided), or transfer the navigation if it is already installed.
Detecting related installed apps
The getInstalledRelatedApps() method allows your website to check whether your iOS/Android/desktop app or PWA is installed on the user's device.
Checking if a related app is installed already helps you to implement features such as hiding custom-installed prompts or redirecting the user directly to the installed app, instead of going to a general purpose website.
To use the getInstalledRelatedApps() method, both the installed app and the website need to configure their connection with each other. Each app, depending on its platform, includes metadata to recognize the website and the website includes the expected installed app in the related_applications field in the manifest.
Tools such as BubbleWrap or PWA Builder, which allow you to publish your PWA to app stores, already add the required metadata so your website can use getInstalledRelatedApps() right away.
To detect whether a PWA is already installed using getInstalledRelatedApps(), define webapp in the manifest related_applications field with the URL to your manifest:
...
"related_applications": [{
"platform": "webapp",
"url": "https://example.com/manifest.json",
}],
...
const relatedApps = await navigator.getInstalledRelatedApps();
const PWAisInstalled = relatedApps.length > 0;
Detect installation from outside the PWA's scope
From Chrome on Android 89, you can detect if a PWA is installed, even from outside the PWA's scope. Your PWA must set a JSON file within the /.well-known/ folder, giving permission to the other scope, as described in this article.
Resources
- Is your app installed? getInstalledRelatedApps() will tell you!
- MDN: Related Applications
- MDN: AppInstalled event
- Apple: Promoting Apps with Smart App Banners
Enhancements
There are many enhancements that can improve the conversion and usage of your PWA.
App shortcuts
App shortcuts is a static list of deep links to your PWA, they are written in your manifest. Web App Manifest spec. It lets you define a list of shortcuts to different parts or features in your PWA, they accelerate navigation to frequently accessed sections.
App shortcuts are available on most desktop operating systems and Android with WebAPK, and they appear in a contextual menu on the app's icon in the home screen, dock, or taskbar, as in the following image:
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/SYm7meKqd286Z62gehX9.png", alt="App shortcuts in Android and macOS.", width="800", height="280" %}
{% Aside %} App shortcuts are only available if the user installs your PWA. {% endAside %}
{% Aside 'warning' %} Once the user has installed your PWA, you can't add or remove shortcuts until you update your manifest on the server and the update algorithm is triggered. {% endAside %}
Shortcuts are defined in the shortcuts member of the manifest. It takes an array of members with the following properties:
name
: The text that will be shown to the user, typically in a context menu.
start_url.
: The URL the PWA should load when the user starts it from this shortcut. It should be a URL within your PWA scope, and it should deep-link to the feature the name or short_name describes.
short_name
(Optional) A shorter name used when there is not enough room to display the full value of the name field.
description
: (Optional) A description of what this shortcut will do
icons
: (Optional) An array of icon objects with src, type, sizes, and optional purpose fields, describing what images should represent the shortcut
You should treat App shortcuts as a best-effort ability. That means that you can't rely on these shortcuts to appear consistently, and even if they appear, you don't know how many shortcuts will appear or if the platform will ignore the icons as its at the discretion of the browsers. A full discussion per platform is out of scope but below you have an idea of how it works on Android and desktop. The best way to deal with this uncertainty is to order the items by priority.
{% Aside 'caution' %} Chrome and Edge on desktop renders the first 10 items, while Chrome on Android only renders the first three shortcuts in the manifest's list, so it's better if you order them by priority. Chrome on Android also adds access to "App Info" and "Site Settings" from the menu after your custom list. {% endAside %}
{% Aside %} If you publish your PWA to app catalogs and stores, you can convert the shortcuts in the manifest to platform-specific implementations using APIs specific to each operating system, such as Quick Actions on iOS and shortcuts on Android. BubbleWrap supports App shortcuts for Android and ChromeOS and PWA Builder for Android, ChromeOS, iOS and iPadOS. {% endAside %}
The following code sample defines different app shortcuts you can try if you install the app on Android with Chrome or on desktop with Edge or Chrome.
{% Glitch 'mlearn-pwa-app-shortcuts' %}
iOS and iPadOS
When publishing PWAs, there are some enhancements that can improve the experience for users on Safari on iOS/iPadOS.
{% Aside 'warning' %} As we mentioned in previous chapters, PWAs on iOS and iPadOS are only installable from Safari; other browsers are not able to install PWAs in the home screen. {% endAside %}
Splash screens
As seen in the Web App Manifest chapter, Android creates splash screens automatically based on the manifest's values. That's not the case for iOS and iPadOS. In these devices, you should define the splash screens in the HTML as static images using <link> elements.
These images are known as startup images on Apple devices and they use the rel property with apple-touch-startup-image value, as in:
<link rel="apple-touch-startup-image" href="ios-startup.png">
The challenge is that the startup image must have the exact window size that your PWA will have on opening. So, different iOS and iPadOS devices will need different images. More situations need to be covered on the iPad, such as landscape/portrait openings and rendering the PWA in multitask mode (such as 1/3,1/2, or 2/3 of the screen).
You can check an updated list of iOS and iPadOS screen sizes at the Apple Human Interface Guidelines
Different versions of the launch image can be set with a media query inside the media attribute:
<link rel="apple-touch-startup-image" href="ios-startup.png"
media="orientation: portrait">
<link rel="apple-touch-startup-image" href="ios-startup-landscape.png"
media="orientation: landscape">
{% Aside 'warning' %} If you want to provide all possible options combining screen definitions, orientations, and multitask mode, you will end up with more than 25 different images you need to create and link in your HTML. If you don't provide a startup image for your user's iOS or iPadOS context, a white screen will appear instead in the opening animation, and while your PWA is loading {% endAside %}
Design patterns for iOS startup images
Defining startup images is hard work, so we have a couple of tools for automated generation and configuration:
- Static generation integrates with your build system, creates all the PNG static images and gives you the HTML code with
<link>elements to inject into your document. PWA Asset Generator is an example of such a tool. - Client-side generator, a JavaScript tool that can embed one or more base64 versions of the startup image into
<link>injected elements based on the current device's type and screen size. You can use an in-memory canvas, render the image and convert it into adata:URI with a PNG file. The PWA Compat library is an easy-to-use client-side library that does this by cloning the Android's typical launch screen.
Detecting a PWA on Apple mobile platforms
While you should use Progressive Enhancement and feature detection in your PWA, there may be some edge cases where we need to know if the user is in a PWA on Apple mobile platforms, such as when you want to offer installation instructions or add links to platform-specific apps that are iOS-only.
To avoid reading the user agent string, check the standalone property of the navigator object. This is a non-standard property, and it's only available on the WebKit engine on iOS and iPadOS.
Fullscreen support
On Safari on iOS and iPads, only display: standalone is supported as a display mode for your PWA's icon. While display: fullscreen is not supported as on Android devices, it's possible to use a non-standard meta-tag to make your PWA enter fullscreen mode.
In the next image, you can see at the left a default standalone design with a theme color, and at the right a PWA with a fullscreen iOS mode that lets you render content behind the status bar.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/SYjZ1upKyQaMuF1nrv7Y.png", alt="A standalone default behavior (left) and a fullscreen iOS screen (right).", width="800", height="216" %}
If you add the following tag in your HTML your PWA on iOS and iPadOS will enter full-screen mode, but it is different from Android.
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
In this mode, the device's status bar (the top where you see the clock, battery level, and notification icons) is still visible but rendered on top of your content with a transparent background.
{% Aside 'warning' %} The theme color is ignored in this mode, and the notification bar is rendered over a transparent background. Also, your app's [0, 0] pixel is now the [0, 0] pixel of the physical screen and not after the safe area. {% endAside %}
When using this mode, be careful with your design because the operating system will always render the icons in white, so you should always contrast your backgrounds for the top of the screen with light content. Also, it's important to use CSS environment variables to render content in the safe area, as seen in App Design Chapter.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/WZSHybaKkUvpLcphGJ8y.png", alt="The top 0px of your viewport is below the status bar by default; if you add a black-translucent meta tag, the top 0px of your viewport will match the physical top of the screen", width="800", height="219" %}
Installation reliability
Safari on iOS and iPadOS before 15.4 only loads the manifest file from the network when the user opens the share sheet -using the share icon within the browser- and not when the page loads. Therefore, Safari doesn't check if your website is a PWA until that moment, which can lead to situations where the manifest can't be loaded or takes too much time, and Safari ignores it.
When Safari can't load the manifest on time, pressing "Add to Home Screen" places an icon on the home screen, but does not provide an app experience; it will merely be a shortcut to a Safari tab.
{% Aside 'warning' %}
If Safari can't load the manifest, it will fall back to check if your PWA has some deprecated meta tags, such as apple-mobile-web-app-capable. You should not use these metatags. They provide a home screen app experience without essential attributes for your PWA, such as honoring the start_url or the scope attributes, making a terrible app experience.
{% endAside %}
Resources
- PWA Asset Generator
- Get things done quickly with app shortcuts
- PWA Compat Library
- Define App Shortcuts
Experimental features
The web is a powerful platform, but there are still gaps in what it can solve. Those who want to develop for the web but need more different capabilities are forced to bundle their web apps in wrappers and publish them in app stores.
Developers may choose to ship their own custom browser as platform-specific apps, which disproportionately increases the size of their web apps. This will also force them to take on the additional security and maintenance burden of keeping both the browser fork and wrapper up to date.
This approach also loses the reach of the web, often being forced to choose what devices and operating systems to support, which often require different wrappers, and which further expands the security and maintenance burden.
Filling these capabilities gaps is the key to fixing this and thereby enabling the web to be the software platform of the future, covering as many use cases as possible, in comparison with platform-specific SDKs.
Project Fugu
This is where the capabilities project, known as Project Fugu, comes in. The capabilities project, or Project Fugu, is a cross-company effort to make it possible for web apps to have the same capabilities as platform-specific apps by exposing the capabilities of these platforms to the web, while maintaining user security, privacy, trust, and other core tenets of the web.
Track capabilities
There is a public tracker to keep up with all the work to ship new capabilities. On the tracker, you can see:
- The status of each API being worked on or considered.
- Platforms the API is targeted for.
- Links and additional information for each API.
- Search and filter capabilities.
- A timeline view.
Request a capability
What if you want to request a new capability? You can file an issue in the Chromium bug tracker, or you can go to webwewant.fyi and fill out a form to reach the corresponding browser vendors.
The process for each capability
Before launch, there are two states an in-progress API could be in, and both allow you to test them.
- A developer trial: the feature is behind a flag, the API isn't necessarily stable, and you shouldn't implement it for real users. You can enable or disable flags on Chromium-based browsers by going to
about:flags, allowing you to test on your own browser instance.
{% Aside 'important' %} Under no circumstances should users ever be told to turn flags on or off. Because such features are still under development, changing flag defaults can have unanticipated consequences for the stability of the browser. {% endAside %}
- An origin trial: a state where features can be enabled by origin, developers can run tests for a wider audience than their single browser instance, but the feature is still being tested and the implementation can change, more details below.
Origin trials
Origin trials allow you to try out new features and give feedback to the web standards community on usability, practicality, and effectiveness. APIs available under origin trial are:
- Experimental, they may change and may become unavailable, including not extending beyond the experiment, and may be unavailable for some time, even if they are eventually enabled for all users. So where possible, you should implement feature detection or graceful degradation to handle the case when the feature is unavailable.
- Stable enough to use with real users, but they may change throughout the course of the trial.
- Limited across all users globally to ensure they don't become a de facto standard, so it's recommended you activate the feature in your codebase following each browser's trial guidelines for a subset of your users.
- Limited to the browser vendor which starts the origin trial, so a Chrome origin trial will not work in Safari, Firefox, or Edge, for instance.
If these requirements are OK for you, you can register an origin to participate in a trial. You can find instructions to sign up for an origin trial in Chrome here and for Microsoft Edge follow this link
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/d5ueJjfbepXwq72v2UjU.png", alt="A list of available origin trials for Google Chrome.", width="800", height="510" %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/hSzBgQsAvgkCUN6h1RQN.png", alt="A list of available origin trials for Microsoft Edge.", width="800", height="541" %}
Safari experimental features
Safari also allows developers to try and test new and unfinished APIs and capabilities, but it doesn't provide origin trials. You can't enable Safari's experimental features for users.
Safari's solution is similar to flags, known as experimental features. Every new version of Safari comes with many experimental features, some of them enabled and some disabled by default.
As a developer, you can change those default settings using:
- The develop, experimental features menu in Safari for macOS.
- The iOS and iPadOS Settings App, by going to Safari, Advanced, Experimental Features.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/Y6agrnMYlpbQ970kYyYp.png", alt="Experimental features available on Safari on iPadOS.", width="800", height="525" %}
Firefox experimental features
Firefox offers experimental features through settings that you can enable or disable by accessing the Configuration Editor using about:config.
Resources
- The capabilities project
- New capabilities status
- Fugu Public Tracker
- Getting started with Chrome origin trials
- Microsoft Edge Origin Trials Guide
- The Web We Want, feedback form
- Firefox experimental features
- WebKit blog
Foundations
A solid foundation is the base requirement for building great PWAs. To implement this foundation you need to design and code for the constraints of the web using a couple of principles:
- Use mobile as a focusing constraint. Make sure each view of your design focuses only on the essential content and interactions.
- Emphasize content and core functionality in the design process.
- Progressively enhance when needed. Start by building a component's core content and functionality with the most straightforward, most widely available tools. Make it accessible. Then, test for advanced features you'd like to use and enhance your component with them.
- Offer a fast and good user experience focused on user-centric web performance metrics, get real user metrics, and push performance for all your users, no matter their network connection, input type, CPU, or GPU power.
{% Aside %} Accessibility is key for a successful PWA. Please follow these guidelines to make sure your app is accessible to anyone, anywhere. {% endAside %}
By following these principles and enhancing with modern patterns and web features, you can create great and fast experiences with truly intrinsic designs. Designs that are powered by constraints instead of pixels, allowing every user access to your content and core functionality in a way that's best suited for their particular browsing context.
Responsive web design
Since Ethan Marcotte's 2010 A List Apart article, Responsive Web Design, designers and developers have been encouraged to create experiences that flex, making user interfaces that work across a spectrum of screen sizes and devices.
Somewhere along the way, though, that became shortened to mobile, tablet, and desktop sizes, with widths heavily influenced by iOS screen sizes. With modern CSS and a renewed focus on responsive design's original intent, we can put the squish back in squishy sites.
Responsive web design introduces three technical ingredients:
- Fluid grids
- Flexible media
- Media queries
Ethan concludes that these technical requirements aren't enough; the way forward also requires a different way of thinking.
The mobile user myth
In the early days of responsive design assumptions were made in the name of making sites easier to design. For example, small experiences were for phones, and they had a 320px width, medium experiences were for tablets, and they had a 1024px width, and anything larger than that was for desktops. Small screens had touch abilities, large screens didn't. Phone users were rushed and distracted and therefore needed a "light" experience.
None of this is true; they are mobile myths perpetuated by the assumption that a user's needs are fundamentally different based solely on screen size or device type. This doesn't hold up to scrutiny.
Take, for example, a social network PWA that you can install today on mobile and desktop. On desktop, many users can keep a narrow window with the feed at one side of the screen while working, and assuming they are on a mobile device because of the available width would be wrong.
The PWA world that is out of the browser's tab is even adding new challenges to a responsive design world, such as the mini-mode and working with foldable devices.
Mini-mode
With a PWA installed on a desktop device, a window can get really small; smaller than a browser's window, smaller than a mobile viewport. This is something new on the web: we can support a mini-mode, a window that can be as small as 200x100 CSS pixels.
When creating a PWA these days, it's a good idea to think about what to offer in mini-mode, thanks to responsive web design, such as just control buttons on a music player, dashboard information, or quick actions.
{% Video src="video/RK2djpBgopg9kzCyJbUSjhEGmnw1/hPR7t2kr42boNc4MfSGQ.mp4", alt="On desktop, a PWA can be rendered in a smaller window than the smallest window you've ever designed for the browser. It adds a new breakpoint for your responsive web design: the mini-mode.", autoplay="true", loop="true", muted="true" %}
On desktop, a PWA can be rendered in a smaller window than the smallest window you've ever designed for the browser. It adds a new breakpoint for your responsive web design: the mini-mode.
Foldables and hybrid
Foldable and hybrid devices are also common these days:
- Small clamshell phones.
- Foldable devices that can be used as phones or tablets.
- Laptops that can be transformed into tablets.
- Tablets that can act as laptops with keyboard and trackpad.
- Phones then can be converted to desktop with a hub.
While the challenge exists for every website, with a Progressive Web App you are in control and responsible for the window when the app is installed. Therefore, your design should react and offer the best experience in every context.
{% Aside %} It's becoming common to transfer an app's navigation between devices, so your PWA can be rendered in a smartwatch after receiving notification; the navigation can be transferred to a phone, and then to a laptop. Supporting all modes and devices is key to avoiding user frustration. {% endAside %}
Everything first
But where should you get started? Mobile first, content first, offline first? When designing for the flexibility of the web, which is it? Well, the answer is yes, everything first. The term mobile first has been interpreted in many ways since Luke Wroblewski first coined it in 2009: from emulating platform-specific UI and UX patterns on the web to building mobile apps before building web apps to just using min-width media queries and calling it a day. Its original intent, though, is this: mobile forces you to focus. As Luke said:
{% Blockquote 'Luke Wroblewski' %} Mobile devices require software development teams to focus on only the most important data and actions in an application. There simply isn't room in a 320 by 480-pixel screen for extraneous, unnecessary elements. You have to prioritize. So when a team designs Mobile First, the result is an experience focused on the key tasks users want to accomplish without the detours and interface debris that litter today's desktop-accessed websites. That's a good user experience and good for business. {% endBlockquote %}
Focus each view of your website on only the essential tasks a user wants to accomplish there, and don't add more stuff to the idea just because they have more screen real estate.
The second principle is hinted at in Responsive Web Design: the "gradient of different experiences." A single, identical, pixel-perfect experience for every single user should not be the goal of your work; it's all but impossible.
{% Aside 'caution' %} There are countless combinations of screen sizes, capabilities, devices, and contexts a user may be browsing with, and as a designer, you do not control what that final mix will be. {% endAside %}
Instead of thinking about your web experiences as a fixed thing, think of it as a set of recommendations that the user's device will use to build the best experience for their current context. To do so, progressive enhancement needs to be embraced.
Progressive enhancement
Progressive Enhancement is a pattern that lets us write code that runs everywhere, starting from standard HTML, CSS, and JavaScript, and adding layers of capabilities on top of that with proper fallbacks when an API is not available.
How do you enhance? Feature detection is a pattern where you perform a test for support and react based on that test results. There are several built-in web platform tools and practices for doing this.
Using @supports, check for browser support of a CSS feature, and apply rules based on the result.
This applies to both CSS properties and values; if a property is supported and a value is not, it will fail, as will an unsupported property. JavaScript code can access this through the CSSSupportsRule
Most new web platform features are attached to existing objects, so ‘feature' in object style detection works well in JavaScript, as will other similar lookups, like checking for properties or methods on elements.
To ship modern JavaScript, you can use the module/nomodule pattern to provide more robust features with a smaller payload to more modern browsers and a fallback experience to older browsers. This has the bonus of guaranteeing a new baseline of JavaScript features, like promises, classes, arrow functions, and const and let are available for browsers that support ES Modules.
You can even combine multiple forms of feature detection to create your enhanced baseline. Coined by the BBC News team, this is called Cutting the Mustard and lets you ship a core experience to everyone and start enhancing the experience only after a specific feature-detected bar is reached.
{% Aside %} The P in PWA comes from Progressive, and it emphasizes the idea of adding features and abilities progressively, and using Feature Detection is key for that goal. {% endAside %}
Avoid device detection
You should directly test for feature support instead of making support assumptions based on the User-Agent string.
User-agent strings have never been truly reliable. They rely on having near-perfect knowledge of every browser, operating system, and device combination in existence to "guess right". Even then they are susceptible to user spoofing, for example, desktop site redirects on mobile browsers are often as simple as spoofing a desktop user-agent string.
Moreover, browsers are working on freezing their User-Agent strings, with user-agent strings for feature detection specifically called out as a reason for deprecation, making them even more unreliable than they previously were for identifying a user and device.
{% Aside %} After saying that device detection is a bad practice, it's also important to clarify that there are still situations where we don't have any other solution, such as detecting iOS devices to offer specific platform instructions. Treat these practices as hacks and be careful to re-test and update these hacks on every new version of the browser on these platforms. {% endAside %}
Content first
Another principle of designing for the web is: start with your content first. For example, a real-time stock ticker with a graph of a stock's prices is, at its core, a table of stocks with their price over some time, maybe with a link to refresh the site.
That can then be enhanced with JavaScript and fetch requests to update the table's values on a timer or enhanced with sockets to provide real-time push-based updates. It can be enhanced again to graph the results, maybe with CSS, maybe with SVG, maybe with Canvas. But the core starts with content.
Intrinsic design
- Mobile as a focusing constraint for user experience.
- Emphasizing content and core functionality in the design process.
- Progressively enhancing with advanced functionality where available.
These principles combine to give us something new: intrinsic design. In her talk Designing Intrinsic Layouts, Jen Simmons talks about using modern CSS tools, like Grid, Flexbox, flow layout, and writing modes, to design and build user interfaces. With these tools, she says:
{% Blockquote 'Jen Simmons' %} You can really make the layout be intrinsic to the content that we have and the design that we want to do. {% endBlockquote %}
This new CSS allows designers to regain some control over layout but do so in a way that aligns with the latest web design principles. Instead of creating fixed forms based on fixed screen sizes, you define content-based rules by which layout happens by tapping into intrinsic properties of that content, like how small or large it can be, the size of the text, and the available space, all at once. They allow you to realize your design as it interacts with your content without needing to control every pixel's placement.
Intrinsic layouts bring the conversation about control on the web full-circle, giving it definition. Control on the web isn't about dictating devices or screen sizes or colors or fonts or layouts or capabilities for each visitor to your site. Control on the web is about writing the rules a browser will use to assemble your experience, building it uniquely for each user in your Progressive Web App.
Web performance
The last but not least important foundation for our PWA is Web Performance. Having a great experience for users is mandatory; it will lead to more conversions in every possible way.
Web performance involves several steps:
- Understand the key user-centric metrics available.
- Set goals for every metric.
- Measure our PWA.
- Improve our metrics by applying techniques and best practices statically in our code or server.
- Measure again.
- Improve the experience to each user, live, based on the user's context.
Web performance metrics today measure how fast your content appears on the screen, but also how interactive your website is and how users perceive the experience.
Web Vitals
During the past decade, we've been dealing with different metrics to measure success in web performance. Today, a set of recommended metrics are grouped under the project Web Vitals. Web Vitals focus metrics in three key areas affecting web performance and perception:
- Loading
- Interactive
- Visual Stability
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/gu948vCUtGMKb6dKcSpZ.png", alt="A set of metrics from Core Web Vitals showing a website's performance results.", width="800", height="225" %}
With Core Web Vitals, you can see at a glance, how good or bad is your PWA in web performance and user experience.
PWA foundations
It's important that your PWA has a solid foundation in responsive design, mobile and everything first, intrinsic design, and web performance as a foundation for a great experience for all your users.
Resources
- Learn Responsive Web Design
- The new responsive: Web design in a component-driven world
- Progressive Enhancement
- Ten modern layouts in one line of CSS
- Web Vitals
- Designing Intrinsic Layouts talk
Getting started
Progressive Web Apps are still websites, with enhanced features and capabilities. They are not attached to a particular technology stack and you can start from scratch with a new site or update your existing website without a complete overhaul. In this guide you'll learn to create a good implementation of the PWA pattern. Here are some strategies to get you started:
Make it installable
Start small! This approach includes beginning with a basic manifest file, a simple offline page, and a service worker to serve the offline page and cache some critical CSS and JavaScript. Thanks to the critical CSS and JavaScript caching, you will get your existing web app ready to work offline while improving its performance.
Focus on a feature
Pick one new feature—such as push notifications or file handling—that will significantly impact your users or your business. This will allow you to dip your toes into the PWA pool without making too many changes at once.
Build a simple version
Take an existing section of your application or a specific user journey, like video playback or access to a boarding pass, and make it work front-to-back as an offline-first PWA, either stand-alone or in context. This allows for a low-stakes experiment enabling you to rethink an experience for your users with PWAs.
Start from the ground up
If you're going through a redesign of your website or can start from scratch, this strategy makes a lot of sense. It enables you to more easily build in PWA design patterns than other strategies, in particular allowing you to take advantage of all the power of service workers from the get-go.
Upgrading a store app
With the ability to publish a PWA to app stores, it's possible to wrap your PWA into a PWA launcher and upload it to stores, such as Google's Play Store or Windows Store. If you have an existing platform-specific app, you can replace it with your PWA published in the store.
With this approach, your existing users get an upgrade of their experiences to the PWA, and new users can still use or install your PWA from the browser, or the app stores. And, you will have one app for everyone, saving costs, time and improving user experience.
PWA checklist
A Progressive Web App is a website, which leads to the question: When does it become a Progressive Web App? The answer is not so simple, as the PWA concept doesn't refer to a specific technology or stack, PWA is instead a pattern including various technical components.
While there are no unique rules among all browsers, there are a set of recommendations, called the Progressive Web App Checklist, to help you create a PWA that users will love.
Core requirements
Because PWAs span all devices, from mobile through desktop, the core Progressive Web App Checklist is about what you need to do to make your app installable and reliable for all users, regardless of screen size or input type.
The core requirements are:
Starts fast, stays fast
Performance plays a significant role in the success of any online experience, as high-performing sites engage and retain users better than poorly performing ones. Sites should focus on optimizing for user-centric performance metrics.
Works in any browser
Progressive Web Apps are web apps first, which means they need to work across browsers, not just in one of them. The experience doesn't have to be identical in all browsers, though. There can be features that aren't supported by one browser, with a fallback to ensure a good experience.
Responsive to any screen size
Users can use your PWA on any screen size, and all content is available at any viewport size.
Provides a custom offline page
When users are offline, keeping them in your PWA provides a more seamless and native-like experience than dropping back to the default browser offline page.
Is installable
Users who install or add apps to their home screens tend to engage with those apps more, and when the PWA is installed it can take advantage of more abilities for a better user experience.
Optimal PWA characteristics
To create a genuinely great Progressive Web App, one that feels like a best-in-class app, you need more than just the core checklist. The optimal Progressive Web App checklist is about making your PWA feel capable and reliable while taking advantage of what makes the web powerful.
Provides an offline experience
By allowing users to use your PWA while offline, you'll create an authentic app-like experience for them. Do this by identifying those features that don't require connectivity, so that users can access at least some functionality.
{% Aside 'gotchas' %} When we say that the PWA should provide an offline experience, it doesn't mean that all the services and content should be available offline. For example a note taking app can't sync its notes when there is no connectivity, but it can allow the users to write the note and sync new changes when they are back online. At the very least you should render the app's user interface with a proper message if your app needs a live connection. {% endAside %}
Is fully accessible
Ensure all the application's content and interactions are understood by screen readers, usable with just a keyboard, that focus is indicated, and color contrast is strong. By making your PWA accessible, you ensure it's usable for everyone.
Uses powerful capabilities where available
From push messaging, WASM, and WebGL to file system access, contact pickers, and integration with app stores. The tools to create highly capable, deeply integrated PWAs are here, allowing you to create a fully-featured user experience, previously reserved for platform apps, that your users can take with them wherever they go.
Is discoverable through search
More than half of all website traffic comes from organic search. Making sure that canonical URLs exist for content and that search engines can index your site is critical for users to find your PWA.
Works with any input type
Users should be able to switch between input types while using your application seamlessly, and input methods should not depend on screen size.
Provides context for permission requests
Only trigger prompts for permissions like notifications, geolocation, and credentials, after providing in-context rationale to improve chances of accepting the prompts from the user.
Follows best practices for healthy code
Keeping your application up-to-date and your codebase healthy makes it easier for you to deliver new features that meet the other goals laid out in this checklist.
{% Aside %} While the PWA Checklist has a set of best practices for all developers, some browsers also have PWA Criteria.
The PWA criteria is a set of technical requirements that your website must comply with to be treated as a PWA. Your PWA will receive special treatment when that happens, such as firing new events, rendering an install badge or dialog, or adding a new Install menu within the browser.{% endAside %}
Resources
Installation
Once the user installs your PWA, it will:
- Have an icon in the launcher, home screen, start menu, or launchpad.
- Appear as a result when a user searches for the app on their device.
- Have a separate window within the operating system.
- Have support for specific capabilities.
{% Aside %} While installation differs per browser and operating system, you don't need to create any kind of package or executable for your PWA to be installed from a browser. {% endAside %}
Installation criteria
Every browser has a criterion that marks when a website or web app is a Progressive Web App and can be installed for a standalone experience. The metadata for your PWA is set by a JSON-based file known as the Web App Manifest, which we will cover in detail in the next module.
As a minimum requirement for installability, most browsers that support it use the Web App Manifest file and certain properties such as the name of the app, and configuration of the installed experience. An exception to this is Safari for macOS, which does not support installability.
The requirements to allow installation differ among the different browsers, this article details the criteria for Google Chrome and includes links to requirements for other browsers.
Because the test that a PWA meets installability requirements can take several seconds, installability itself may not be available as soon as a URLs response is received.
{% Aside %} The process for uninstalling a PWA is different on each combination of operating system and browser. In most situations, the app can be uninstalled the same as any other platform-specific app; in other situations, the PWA window offers a menu with an uninstall option. Deleting the icon may not clear the storage that a PWA is using. {% endAside %}
Desktop installation
Desktop PWA installation is currently supported by Google Chrome and Microsoft Edge on Linux, Windows, macOS, and Chromebooks. These browsers will show an install badge (icon) in the URL bar (see the image below), stating that the current site is installable.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/aNv7ezpI3iO6YpIlHt9J.png", alt="Chrome and Edge on desktop with the install badge in the URL bar", width="800", height="751" %}
When a user is engaged with a site, they may see a popup such as the one below inviting users to install it as an app.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/YVHGuBQDUKGZXT7cGuHO.png", alt="Google chrome in-product help suggesting the PWA installation.", width="800", height="366" %}
The browser's drop-down menu also includes an "Install " item that the user can use:
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/WBZ15ArNciZnrvJY6yUt.png", alt="Chrome and Edge menu items for PWA installation.", width="800", height="533" %}
{% Aside %}
Chromium-based browsers on desktop let you see all the installed PWAs with that browser using about:apps.
{% endAside %}
Only standalone and minimal-ui display modes are supported on desktop operating systems.
PWAs installed on desktops:
- Have an icon in the Start menu or Start screen on Windows PCs, in the dock or desktop in Linux GUIs, in the macOS launchpad, or a Chromebook's app launcher.
- Have an icon in app switchers and docks when the app is active, was recently used, or is opened in the background.
- Appear in the app search, for example, search on Windows or Spotlight on macOS.
- Can set a badge number on their icons, to indicate new notifications. This is done with the Badging API.
- Can set a contextual menu for the icon with App Shortcuts.
- Can't be installed twice with the same browser.
{% Aside %} Installation may unlock other capabilities that are not available for PWAs running within the browser's tab. We'll talk more about this in the Capabilities Chapter. {% endAside %}
After installing an app on the desktop, users can navigate to about:apps, right-click on a PWA, and select "Start App when you Sign In" if they want your app to open automatically on startup.
iOS and iPadOS installation
A browser prompt to install your PWA doesn't exit On iOS and iPadOS. In these platforms PWAs are also known as home screen web apps. These apps have to be added manually to the home screen via a menu that is available only in Safari. It is recommended that you add the tag apple-touch-icon to your html.
To define the icon, include the path to your icon to your HTML <head> section, like this:
<link rel="apple-touch-icon" href="/icons/ios.png">
Safari will use that information to create the shortcut and if you don't provide a specific icon for Apple devices, the icon on the home screen will be a screenshot of your PWA when the user installed it.
It's important to understand that PWA installation is only available if the user browses your website from Safari. Other browsers available in the App Store, such as Google Chrome, Firefox, Opera, or Microsoft Edge, cannot install a PWA on the home screen.
The steps to add apps to the home screen are:
{% Video src="video/RK2djpBgopg9kzCyJbUSjhEGmnw1/UhWxRAtIB6KQpbMYnDSe.mp4", autoplay="true", controls="true" %}
On iOS and iPadOS, only the standalone display mode is supported. Therefore, if you use minimal UI mode, it will fall back to a browser shortcut; if you use fullscreen, it will fall back to standalone.
{% Aside 'caution' %} On Apple devices, you can install the same PWA multiple times; each installation will have its own isolated storage, and it will be treated as a different app. {% endAside %}
PWAs installed on iOS and iPadOS:
- Appear in the home screen, Spotlight's search, Siri Suggestions, and App Library search.
- Don't appear in App Gallery's categories folders.
- Lack support for capabilities such as badging and app shortcuts.
Incidentally, Safari uses a native technology known as Web Clips to create the PWA icons in the operating system. They are just XML files in Apple's Property List format stored in the filesystem.
{% Aside 'gotchas' %} When users use the PWA, they are not using an instance of the Safari app (like they use an instance of Chrome when doing so), they are using something known as Web.app, so you should expect slight differences between your PWA rendered in Safari and a PWA window. Both Safari and Web.app use the same core from WebKit and the same JavaScript runtime, but they run in different processes and may have different implementations, such as having isolated storage. {% endAside %}
Android installation
On Android, PWA install prompts differ by device and browser. Users may see:
- Variations in the wording of the menu item for install such as Install or Add to Home Screen.
- Detailed installation dialogs.
In the following image you can see two different versions of an installation dialog, a simple mini-infobar (left) and a detailed installation dialog (right).
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/qqVFLVmurQkr5RGauvGK.png", alt="Mini info bar and installation dialogs on Android.", width="800", height="417" %}
Depending on the device and browser, your PWA will either be installed as a WebAPK, a shortcut, or a QuickApp.
WebAPKs
A WebAPK is an Android package (APK) created by a trusted provider of the user's device, typically in the cloud, on a WebAPK minting server. This method is used by Google Chrome on devices with Google Mobile Services (GMS) installed, and by Samsung Internet browser, but only on Samsung-manufactured devices, such as a Galaxy phone or tablet. Together, this accounts for the majority of Android users.
When a user installs a PWA from Google Chrome and a WebAPK is used, the minting server "mints" (packages) and signs an APK for the PWA. That process takes time, but when the APK is ready, the browser installs that app silently on the user's device. Because trusted providers (Play Services or Samsung) signed the APK, the phone installs it without disabling security, as with any app coming from the store. There is no need for sideloading the app.
PWAs installed via WebAPK:
- Have an icon in the app launcher and home screen.
- Appear on Settings, Apps.
- Can have several capabilities, such as badging, app shortcuts, and capture links within the OS.
- Can update the icon and app's metadata.
- Can't be installed twice.
{% Aside 'gotchas' %} You, as a PWA developer, don't need to build or sign any package when a WebAPK is used. It's all handled transparently between the browser, device, and minting server. {% endAside %}
Shortcuts
While WebAPKs provide the best experience for Android users, they can't always be created. When they can't, browsers fall back to creating a website shortcut. Because Firefox, Microsoft Edge, Opera, Brave, and Samsung Internet (on non-Samsung devices) don't have minting servers they trust, they'll create shortcuts. Google Chrome will too if the minting service is unavailable or your PWA doesn't meet installation requirements.
PWAs installed with shortcuts:
- Have a browser-badged icon on the home screen (see the following examples).
- Don't have an icon in the Launcher or on Settings, Apps.
- Can't use any capabilities that require installation.
- Can't update their icons and app metadata.
- Can be installed many times, even using the same browser; when this happens, all will point to the same instance, and use the same storage.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/79QaL12M28cHhIrJLRyM.png", alt="A PWA installed with different browsers on the same device.", width="800", height="432" %}
QuickApps
Some manufacturers, including Huawei and ZTE, offer a platform known as QuickApps to create light web apps similar to PWAs but using a different technology stack. Some browsers on these devices, like the Huawei browser, can install PWAs that get packaged as QuickApp, even if you are not using the QuickApp stack.
When your PWA is installed as a QuickApp, users will get a similar experience to the one they would have with shortcuts, but with an icon badged with the QuickApps icon (a lightning image). The app will also be available to launch from the QuickApp Center.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/iHhjBvDriV4hZVnmVapW.png", alt="QuickApps on a Huawei or ZTE home screen.", width="800", height="497" %}
Prompting for installation
In Chromium-based browsers on desktop and Android devices, it's possible to trigger the browser's installation dialog from your PWA. The Installation Prompt chapter, will cover patterns for doing so and how to implement them.
{% Aside 'warning' %} If you provide instructions to the user, remember that menu items' names vary by language. {% endAside %}
App catalogs and stores
Your PWA can also be listed in app catalogs and stores to increase its reach and let users find it in the same place they find other apps. Most app catalogs and stores support technologies that let you publish a package that doesn't include the whole web app (such as your HTML and assets). These technologies let you create just a launcher to a standalone web rendering engine that will load the app and let the service worker cache the necessary assets.
The app catalogs and stores that support publishing a PWA are:
- Google Play Store for Android and ChromeOS, using a Trusted Web Activity.
- Apple App Store for iOS, macOS, and iPadOS, using WKWebView and App-Bound Domains.
- Microsoft Store for Windows 10 and 11, using APPX packages.
- Samsung Galaxy Store, using the Samsung WebAPK minting server.
- Huawei AppGallery, using a QuickApp container for your HTML application.
If you want to learn more about how to publish a PWA to app catalogs and stores, check out BubbleWrap CLI and PWA Builder.
{% Aside 'warning' %} Some app catalogs and stores have technical or business requirements that before your PWA is accepted for publication. Check each store's requirements before starting. {% endAside %}
Resources
- What does it take to be installable
- WebAPKs on Android
- Patterns for promoting PWA installation
- Using a PWA in your Android app
- List your Progressive Web App in Google Play
- Submit your PWA to the Microsoft Store
- Publishing a PWA to App Store
- WebKit: App-Bound Domains
Installation prompt
Users may not be familiar with the PWA install process. As the developer, you will understand when it is the right time to invite the user to install the app. The default browser installation prompts can also be enhanced. Let's check out the tools available.
Enhancing the install dialog
Browsers provide default installation prompts when PWAs pass the install criteria. The browser uses the name and icons properties from your Web App Manifest to build the prompt.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/Ekh5btJPBqmnX85DonZK.png", alt="Microsoft Edge installation prompt.", width="800", height="426" %}
Some browsers enhance the installation prompt experience using the promotional fields in the manifest, including description, categories, and screenshots. For example, using Chrome on Android, if your PWA provides values for the description and screenshots fields, the installation dialog experience transforms from a small Add to home screen info bar to a bigger, more detailed dialog, similar to the install prompts from an app store.
{% Video src="video/RK2djpBgopg9kzCyJbUSjhEGmnw1/eU7EiYjZKtVsBrQ3xfBz.mp4" %}
See promotional fields in action:
{% Glitch 'learn-pwa-web-app-manifest-promotional' %}
The beforeinstallprompt event
The browser's installation prompts are the first step in getting users to install your PWA. To implement your own install experience, your app still needs to pass the install criteria: when the browser detects that your app is installable, it fires the beforeinstallprompt event. You need to implement this event handler to customize the user's experience. Here's how:
- Listen for the
beforeinstallpromptevent. - Save it (you'll need it later).
- Trigger it from your UI.
{% Aside 'caution' %} Not all browsers support this event, and it has been moved from the Web App Manifest spec to a separate incubator. {% endAside %}
Check the code below for an example of an event listener for the beforeinstallprompt event, its capture and later custom use.
// This variable will save the event for later use.
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevents the default mini-infobar or install dialog from appearing on mobile
e.preventDefault();
// Save the event because you'll need to trigger it later.
deferredPrompt = e;
// Show your customized install prompt for your PWA
// Your own UI doesn't have to be a single element, you
// can have buttons in different locations, or wait to prompt
// as part of a critical journey.
showInAppInstallPromotion();
});
Then, if the user clicks on your customized install button, use the deferredPrompt that has previously been saved and call its prompt() method, because the user still needs to go through the browser's process to install your app. What you did was delay the event until you gave the user the right context to encourage them to install the PWA.
Capturing the event gives you the opportunity to add hints and incentives for your users to install your app, and to choose to prompt for installation when you know the users are more engaged.
{% Aside 'caution' %}
You can only call prompt() on the deferred event once. If the user dismisses it, you need to wait until the beforeinstallprompt event is fired again.
{% endAside %}
The event will not fire if:
- The user has already installed the current PWA (valid only for desktop and WebAPK on Android).
- The app does not pass the PWA installation criteria.
- The PWA is not installable on the current device for other reasons (for example, a device in kiosk mode or without permissions).
{% Aside 'gotchas' %}
Chrome and Edge on iOS and iPadOS do not support PWA installation, so the beforeinstallprompt event can't fire. In this case, the only option is to open the PWA using Safari, where it is installable from the share, add to the home screen menu.
{% endAside %}
The best place to prompt
Where to prompt depends on your application and when your users are most engaged with your content and services. When you capture the beforeinstallprompt, you can guide users through the reasons to keep using your app and the advantages they will gain from installing it.
You can choose to display install hints anywhere in your app. Some common patterns are: in the side menu, after a critical user journey such as completing an order, or after a sign-up page. You can read more about this in Patterns for promoting PWA installation.
Gathering analytics
Using analytics will help you to understand better where and when to present your prompts. You can use the userChoice property from the beforeinstallprompt event; userChoice is a promise that will resolve with the action the user took.
// Gather the data from your custom install UI event listener
installButton.addEventListener('click', async () => {
// deferredPrompt is a global variable we've been using in the sample to capture the `beforeinstallevent`
deferredPrompt.prompt();
// Find out whether the user confirmed the installation or not
const { outcome } = await deferredPrompt.userChoice;
// The deferredPrompt can only be used once.
deferredPrompt = null;
// Act on the user's choice
if (outcome === 'accepted') {
console.log('User accepted the install prompt.');
} else if (outcome === 'dismissed') {
console.log('User dismissed the install prompt');
}
});
{% Aside %}
Chromium browsers trigger the appinstalled event on the window object. The event is fired when the user accepts the install, whether action was triggered by a custom install button or by the browser.
{% endAside %}
See it in action
Try the following sample in action on a Chromium browser (desktop or Android).
{% Glitch 'mlearn-pwa-web-app-install-prompt' %}
Fallback
If the browser doesn't support the beforeinstallprompt or the event does not fire, there is no other way to trigger the browser's installation prompt. However, on platforms that allow the user to install PWAs manually, like iOS, you can display these instructions to the user.
You should only render these instructions in browser mode; other display options, such as standalone or fullscreen mean the user has already installed the app.
To render the element only in browser mode, use the display-mode media query:
#installInstructions {
display: none
}
@media (display-mode: browser) {
#installInstructions {
display: block
}
}
Codelab
{% Aside 'codelab' %} Try it yourself with the Make it installable codelab. {% endAside %}
Libraries
Check out these libraries for help with rendering a custom install prompt:
Resources
- Patterns for Promoting PWA installation
- How to provide your own in-app install experience
- MDN: Add to Home Screen
- Web Manifest Incubations
Offline data
To build a solid offline experience, your PWA needs storage management. In the caching chapter you learned that cache storage is one option to save data on a device. In this chapter, we'll show you how to manage offline data, including data persistence, limits, and the available tools.
Storage
Storage is not just about files and assets, but can include other types of data. On all browsers that support PWAs, the following APIs are available for on-device storage:
- IndexedDB: A NoSQL object storage option for structured data and blobs (binary data).
- WebStorage: A way to store key/value string pairs, using local storage or session storage. It's not available within a service worker context. This API is synchronous so it's not recommended for complex data storage.
- Cache Storage: As covered in the Caching module.
{% Aside %} On supported platforms, you can also use the FileSystem Access API to read and write to and from the user's local filesystem, with permission. Since you are writing files directly to the file system the browser won't assign the data to your origin's quota, and any other application can interact with those files, if the user allows it. {% endAside %}
You can manage all device storage with the Storage Manager API on supported platforms. The Cache Storage API and IndexedDB provide asynchronous access to persistent storage for PWAs and can be accessed from the main thread, web workers, and service workers. Both play essential roles in making PWAs work reliably when the network is flaky or non-existent. But when should you use each?
Use the Cache Storage API for network resources, things you'd access by requesting them via a URL, such as HTML, CSS, JavaScript, images, videos, and audio.
Use IndexedDB to store structured data. This includes data that needs to be searchable or combinable in a NoSQL-like manner, or other data such as user-specific data that doesn't necessarily match a URL request. Note that IndexedDB isn't designed for full-text search.
{% Aside 'caution' %} All storage, no matter how you use it, is tied not just to the PWA it is used for, but to the PWA's origin, too. Therefore, if you deploy more than one PWA to an origin (which is not recommended), keep in mind that IndexedDB databases, cache storage, and the other browser storage will share quotas across all service workers and PWAs. {% endAside %}
IndexedDB
To use IndexedDB, first open a database. This creates a new database if one does not exist.
IndexedDB is an asynchronous API, but it takes a callback instead of returning a Promise. The following example uses Jake Archibald's idb library, which is a tiny, Promise wrapper for IndexedDB. Helper libraries are not required to use IndexedDB, but if you want to use the Promise syntax the idb library is an option.
The following example creates a database to hold cooking recipes.
Creating and opening a database
To open a database:
- Use the
openDBfunction to create a new IndexedDB database calledcookbook. Because IndexedDB databases are versioned, you need to increase the version number whenever you make changes to the database structure. The second parameter is the database version. In the example is set to 1. - An initialization object containing an
upgrade()callback is passed toopenDB(). The callback function is called when the database is installed for the first time or when it upgrades to a new version. This function is the only place where actions can happen. Actions might include creating new object stores (the structures IndexedDB uses to organize data), or indexes (that you'd like to search on). This is also where data migration should happen. Typically, theupgrade()function contains aswitchstatement withoutbreakstatements to allow each step to happen in order, based on what the old version of the database is.
import { openDB } from 'idb';
async function createDB() {
// Using https://github.com/jakearchibald/idb
const db = await openDB('cookbook', 1, {
upgrade(db, oldVersion, newVersion, transaction) {
// Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
switch(oldVersion) {
case 0:
// Placeholder to execute when database is created (oldVersion is 0)
case 1:
// Create a store of objects
const store = db.createObjectStore('recipes', {
// The `id` property of the object will be the key, and be incremented automatically
autoIncrement: true,
keyPath: 'id'
});
// Create an index called `name` based on the `type` property of objects in the store
store.createIndex('type', 'type');
}
}
});
}
The example creates an object store inside the cookbook database called recipes, with the id property set as the store's index key and creates another index called type, based on the type property.
Let's take a look at the object store that's just been created. After adding recipes to the object store and opening DevTools on Chromium-based browsers or Web Inspector on Safari, this is what you should expect to see:
{% Img src="image/SeARmcA1EicLXagFnVOe0ou9cqK2/OdDiXwJBMrsOgS1Phoz7.png", alt="Safari and Chrome showing IndexedDB contents.", width="800", height="538" %}
Adding data
IndexedDB uses transactions. Transactions group actions together, so they happen as a unit. They help ensure that the database is always in a consistent state. They're also critical, if you have multiple copies of your app running, for preventing simultaneous writing to the same data. To add data:
- Start a transaction with the
modeset toreadwrite. - Get the object store, where you'll add data.
- Call
add()with the data you are saving. The method receives data in dictionary form (as key/value pairs) and adds it to the object store. The dictionary must be cloneable using Structured Cloning. If you wanted to update an existing object, you'd call theput()method instead.
Transactions have a done promise that resolves when the transaction completes successfully, or rejects with a transaction error.
As the IDB library documentation explains, if you're writing to the database, tx.done is the signal that everything was successfully committed to the database. However, it's beneficial to await individual operations so that you can see any errors that cause the transaction to fail.
// Using https://github.com/jakearchibald/idb
async function addData() {
const cookies = {
name: "Chocolate chips cookies",
type: "dessert"
cook_time_minutes: 25
};
const tx = await db.transaction('recipes', 'readwrite');
const store = tx.objectStore('recipes');
store.add(cookies);
await tx.done;
}
Once you've added the cookies, the recipe will be in the database with other recipes. The ID is automatically set and incremented by indexedDB. If you run this code twice you will have two identical cookie entries.
{% Glitch 'learn-pwa-offline-data-saving' %}
Retrieving data
Here is how you get data from IndexedDB:
- Start a transaction and specify the object store or stores, and optionally transaction type.
- Call
objectStore()from that transaction. Make sure you specify the object store name. - Call
get()with the key you want to get. By default the store uses its key as an index.
// Using https://github.com/jakearchibald/idb
async function getData() {
const tx = await db.transaction('recipes', 'readonly')
const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
const value = await store.get([id]);
}
{% Glitch 'learn-pwa-offline-data-get' %}
{% Aside 'codelab' %} Progressive Web Apps: IndexedDB. {% endAside %}
The storage manager
Knowing how to manage your PWA's storage is particularly important to storing and streaming network responses correctly.
Storage capacity is shared among all storage options, including Cache Storage, IndexedDB, Web Storage, and even the service worker file and its dependencies.
However, the amount of storage available varies from browser to browser. You're not likely to run out; sites could store megabytes and even gigabytes of data on some browsers. Chrome, for instance, allows the browser to use up to 80% of the total disk space, and an individual origin can use up to 60% of the entire disk space. For browsers that support the Storage API, you can know how much storage is still available for your app, its quota, and its use.
The following example uses the Storage API to get estimate quota and usage, then calculates the percentage used and remaining bytes. Note that navigator.storage returns an instance of StorageManager. There is a separate Storage interface and it is easy to get them confused.
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
{% Glitch 'learn-pwa-offline-data-storage' %}
In Chromium DevTools, you can see your site's quota and how much storage is used broken down by what's using it, by opening the Storage section in the Application tab.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/NAXXvUU099wqtSbzvt9m.png", alt="Chrome DevTools in Application, Clear Storage section", width="800", height="567" %}
Firefox and Safari don't offer a summary screen for seeing all storage quota and usage for the current origin.
Data persistence
You can ask the browser for persistent storage on compatible platforms to avoid automatic data eviction after inactivity or on storage pressure. If granted, the browser will never evict data from storage. This protection includes the service worker registration, IndexedDB databases, and files in cache storage. Note that users are always in charge, and they can delete the storage at any time, even if the browser has granted persistent storage.
To request persistent storage, call the StorageManager.persist(). As before, the StorageManager interface is access through the navigator.storage property.
async function persistData() {
if (navigator.storage && navigator.storage.persist) {
const result = await navigator.storage.persist();
console.log(`Data persisted: ${result}`);
}
You can also check if persistent storage is already granted in the current origin by calling StorageManager.persisted(). Firefox requests permission from the user to use persistent storage. Chromium-based browsers give or deny persistence based on a heuristic to determine the importance of the content for the user. One criteria for Google Chrome is, for example, PWA installation. If the user has installed an icon for the PWA in the operating system, the browser may grant persistent storage.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/W89sHSgKqkh9xV8nVD0C.png", alt="Mozilla Firefox asking the user for storage persistence permission.", width="800", height="279" %}
{% Aside 'caution' %}
The StorageManager interface has persist() and persisted() methods that both return Promises that resolve with a boolean. The persist() function asks for enabling persistent storage and returns the result, and persisted() returns the current state without affecting it.
{% endAside %}
{% Aside %} Content Indexing API is an experimental API that's available in some Chromium browsers which lets your PWA expose the pieces of content that you have indexed offline. If you register your offline-ready content using this API, the user will see them in Downloads within the browser even when there is no network connection. {% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/b1o5a6iCgjqOVMVfcbuj.png", alt="The Content Indexing API results available in Chrome for Android.", width="800", height="1480" %} {% endAside %}
API Browser support
Web Storage
{% BrowserCompat 'api.Window.localStorage' %}
File System Access
{% BrowserCompat 'api.FileSystemHandle' %}
Storage Manager
{% BrowserCompat 'api.StorageManager' %}
Resources
- Storage on the Web
- FileSystem Access API
- Persistent Storage
- MDN: Storage API
- MDN: Client-side storage
- Content Indexing
OS Integration
Web apps have a big reach. They run on multiple platforms. They are easy to share via links. But traditionally they lacked integration with the operating system. Not long ago they were not even installable. Luckily that has changed and now we can take advantage of that integration to add useful features to our PWAs. Let's explore some of those options.
Working with the file system
A typical user workflow using files looks like this:
- Pick a file or folder from the device and open it directly.
- Make changes to those files or folders, and save the changes back directly.
- Make new files and folders.
Before the File System Access API, web apps couldn't do this. Opening files required a file upload, saving changes required users to download them, and the web had no access at all to make new files and folders in the user's filesystem.
Opening a file
To open a file we use the window.showOpenFilePicker() method. Note that this method requires a user gesture, such as a button click. Here is the rest of the setup for opening a file:
- Capture the file handle from the file system access's file picker API. This gives you basic information about the file.
- Using the handle's
getFile()method, you'll get a special kind ofBlobcalled aFilethat includes additional read-only properties (such as name and last modified date) about the file. Because it's a Blob, Blob methods can be called on it, such astext(), to get its content.
// Have the user select a file.
const [ handle ] = await window.showOpenFilePicker();
// Get the File object from the handle.
const file = await handle.getFile();
// Get the file content.
// Also available, slice(), stream(), arrayBuffer()
const content = await file.text();
Saving changes
To save changes to a file, you also need a user gesture; then:
- Use the file handle to create a
FileSystemWritableFileStream. - Make changes to the stream. This won't update the file in place; instead, a temporary file is typically created.
- Finally, when you've finished making changes, you close the stream, which moves the changes from temporary to permanent.
Let's see this in code:
// Make a writable stream from the handle.
const writable = await handle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the file and write the contents to disk.
await writable.close();
{% Glitch 'mlearn-pwa-os-integration-file' %}
{% Aside 'caution' %} Safari implements the File System Access API, not for the public system, but as an origin-private sandbox. You can only read and write files in that virtual file system using this API. {% endAside %}
File handling
The File System Access API lets you open files from within your app, but what about the other way around? Users want to set their favorite app as their default to open files with. The file handling API is an experimental API that lets installed PWAs: Register as a file handler on a user's device, specifying the MIME type and file extension that your PWA supports in your web app manifest. You can specify custom file icons for your supported extensions.
Once registered, your installed PWA will show up as an option from the user's file system, allowing them to open the file directly into it. Here is an example of the manifest setup for a PWA to read text files:
...
"file_handlers": [
{
"action": "/open-file",
"accept": {
"text/*": [".txt"]
}
}
]
...
URL handling
With URL handling, your PWA can capture links that are part of its scope from the operating system and render them within a PWA window, instead of the default browser's tab. For example, if you receive a message linking to the PWA, or click on a deep link (a URL that points to a specific piece of content) in your PWA, the content will open in a standalone window.
This behavior is automatically available on Android when WebAPK is used, such as when users install a PWA with Chrome. It's impossible to capture URLs on PWAs installed on iOS and iPadOS from Safari.
For desktop browsers, the web browser community created a new spec. This spec is currently experimental; it adds a new manifest file member: url_handlers. This property expects an array of origins that the PWA wants to capture. The origin of your PWA will be granted automatically, and each other origin must accept that handling operating through a file named web-app-origin-association.
For example, if your PWA's manifest is hosted on the web.dev, and you want to add the app.web.dev origin, it would look like this:
"url_handlers": [
{"origin": "https://app.web.dev"},
]
In this case, the browser will check if a file exists at app.web.dev/.well-known/web-app-origin-association, accepting the URL handling from the PWA scope URL. The developer has to create this file. In the following example, the file looks like this:
{
"web_apps": [
{
"manifest": "/mypwa/app.webmanifest",
"details": {
"paths": [ "/*" ]
}
}
]
}
{% Aside 'gotchas' %} On Android with WebAPKs, PWAs are automatically registered as handlers for the manifest's scope with an Android intent filter, and it is not possible to add more origins or scopes to it. For iOS and iPadOS, you can only handle URLs with PWAs published in the AppStore, such as those created with PWABuilder. {% endAside %}
URL protocol handling
URL handling works with standard https protocol URLs, but it is possible to use custom URI-schemes, such as pwa://. In several operating systems, installed apps gain this ability by apps registering their schemes.
For PWA, this capability is enabled using the URL protocol handler API, available only on desktop devices. You can only allow custom protocols for mobile devices by distributing your PWA on app stores.
{% Aside 'caution' %}
For PWAs using the URL protocol handler API, the protocol name has to be one of the safelisted schemes, or it must use a web+ prefix. So, in our example, a link should point to web+pwa:// to open our PWA.
{% endAside %}
To register, you can use the registerProtocolHandler() method, or use the protocol_handlers member in your manifest, with the desired scheme and the URL you want to load in your PWA's context, such as:
...
{
"protocol_handlers": [
{
"protocol": "web+pwa",
"url": "/from-protocol?value=%s"
},
]
}
...
You can route the URL from-protocol to the correct handler and get the query string value in your PWA. The %s is a placeholder for the escaped URL that triggered the operation, so if you have a link somewhere such as <a href="web+pwa://testing">, your PWA will open /from-protocol?value=testing.
Calling other apps
You can use URI schemes to connect to any other installed app (PWA or not) in users' devices on every platform. You just need to create a link or use navigator.href and point to the URI scheme you want, passing the arguments in URL-escaped form.
You can use well known standard schemes, such as tel: for phone calls, mailto: for email sending, or sms: for text messaging; or you can learn about other apps' URL schemes, for example from well known messaging, maps, navigation, online meetings, social networks, and app stores.
{% Aside %} Instead of using private URI schemes to communicate with messaging and social media apps, you can use the generic Web Share API {% endAside %}
Web Share
{% BrowserCompat 'api.Navigator.share' %}
The API is only available on operating systems with a share mechanism, including Android, iOS, iPadOS, Windows, and ChromeOS.
You can share an object containing:
- Text (
titleandtextproperties) - A URL (
urlproperty) - Files (
filesproperty).
To check if the current device can share, for simple data, like text, you check for the presence of the navigator.share() method, to share files you check for the presence of the navigator.canShare() method.
You request the share action by calling navigator.share(objectToShare). That call returns a Promise that resolves with undefined or rejects with an exception.
{% Glitch 'mlearn-pwa-os-integration-web-share' %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/wpB9v8QQTw4wB1iEGqvT.png", alt="Chrome on Android and Safari on iOS opening the Share Sheet thanks to Web Share.", width="800", height="661" %}
Web Share Target
It's currently available on Android with WebAPK and ChromeOS, and it works only after the user has installed your PWA. The browser registers the share target within the operating system when the app is installed.
action
: URL that will be loaded in a PWA window that is expected to receive the shared data.
method
: HTTP verb method will be used for the action, such as GET, POST, or PUT.
enctype
: (Optional) Encoding type for the parameters, by default is application/x-www-form-urlencoded, but it can also be set as multipart/form-data for methods such as POST.
params
: An object that will map share data (from the keys: title, text, url and files from Web Share) to arguments that the browser will pass in the URL (on method: 'GET') or in the body of the request using the selected encoding.
{% Aside %}
You can parse a POST request server-side or within your service worker if you want to avoid a trip to the network. Check this sample for further details.
{% endAside %}
For example, you can define for your PWA that you want to receive shared data (title and url only) by adding in your manifest:
...
"share_target": {
"action": "/receive-share/",
"method": "GET",
"params": {
"title": "shared_title",
"url": "shared_url"
}
}
...
From the previous sample, if any app in the system is sharing a URL with a title, and the user picks your PWA from the dialog, the browser will create a new navigation to your origin's /receive-share/?shared_title=AAA&shared_url=BBB, where AAA is the shared title, and BBB is the shared URL. You can use JavaScript to read that data from the window.location string by parsing it with the URL constructor.
The browser will use the PWA name and icon from your manifest to feed the operating system's share entry. You can't pick a different set for that purpose.
{% Aside %} Make sure the page that handles the shared data is accessible offline. You don't want users choosing your app and finding an error because they don't have a connection at the moment. {% endAside %}
For more detailed examples and how to receive files, check Receiving shared data with the Web Share Target API
Contact Picker
{% BrowserCompat 'api.ContactsManager' %}
With the Contact Picker API, you can request the device to render a native dialog with all the user's contacts so the user can choose one or more. Your PWA can then receive the data you want from those contacts.
The Contact Picker API is mainly available on mobile devices, and everything is done through the navigator.contacts interface on compatible platforms.
You can request the available properties to query with navigator.contacts.getProperties(), and request a single or multiple contact selection with a list of desired properties.
Some sample properties are name, email, address, and tel. When you ask the user to pick one or more contacts, you can call navigator.contacts.select(properties), passing an array of properties you want to get in return.
{% Aside 'caution' %} The API doesn't guarantee that the returned objects will have all the requested properties, so you should recheck for their presence. {% endAside %}
The following sample will list the contacts received by the picker.
async function getContacts() {
const properties = ['name', 'email', 'tel'];
const options = { multiple: true };
try {
const contacts = await navigator.contacts.select(properties, options);
console.log(contacts);
} catch (ex) {
// Handle any errors here.
}
}
{% Glitch 'mlearn-pwa-os-integration-contacts' %}
Resources
- The File System Access API: simplifying access to local files
- Let installed web applications be file handlers
- Handle files in Progressive Web Apps
- Integrate with the OS Sharing UI with the Web Share API
- Share content with other apps
- Receiving shared data with the Web Share Target API
- A contact picker for the web
Progressive Web Apps
A Progressive Web App (PWA) is a web app that uses progressive enhancement to provide users with a more reliable experience, uses new capabilities to provide a more integrated experience, and can be installed. And, because it's a web app, it can reach anyone, anywhere, on any device, all with a single codebase. Once installed, a PWA looks like any other app, specifically:
- It has an icon on the home screen, app launcher, launchpad, or start menu.
- It appears when you search for apps on the device.
- It opens in a standalone window, wholly separated from a browser's user interface.
- It has access to higher levels of integration with the OS, for example, URL handling or title bar customization.
- It works offline.
The web platform
The web is an incredible platform. Its mix of universality across devices and operating systems, its user-centered security model, and the fact that no single company controls its specification or implementation make it a powerful platform for delivering software.
Combined with the web's inherent linkability, it's possible to search across it and share what you've found with anyone, anywhere. Whenever you go to a website, it's the latest version the publisher deployed, and your experience with that site can be as temporary or as permanent as you'd like.
Web applications can reach anyone, anywhere, on any device with a single codebase. For developers, the web also offers a transparent and straightforward deploying mechanism. There is no need for packaging, no extra content review, or delays on updates. Users always get the latest version when they visit your app. With new capabilities and techniques, a web app can now allow you to interact or see content even when offline, a hurdle that was impossible to overcome a few years ago.
Platform-specific apps
Platform-specific apps, on both mobile and desktop, are known for being rich and reliable. They're ever-present, on home screens, docks, and taskbars. They work regardless of network connection, and launch in their own standalone experience. They can read and write files from the local file system, access hardware connected via USB, serial, or Bluetooth, and interact with data stored on your devices, such as contacts and calendar events. In platform-specific applications, you can take pictures, play songs listed on the home screen, or control media playback while in another app. These applications feel like part of the device they run on.
{% Aside %} In modern mobile operating systems, platform-specific apps are installed mostly from app stores, with rules and limitations on who can publish and what can be published for their users. These apps are typically shipped as a large, indivisible package, and every update needs re-packaging, re-signing, re-approval, and on-device re-installation. {% endAside %}
A challenge for platform-specific apps is that they are not compatible with multiple platforms and devices, so it's not easy, if even possible, to move an Android app to iOS or an iOS to Windows or ChromeOS without creating a new app from scratch.
Bringing the best of both worlds
If you think about platform apps and web apps in terms of capabilities and reach, platform apps represent the best of capabilities, whereas web apps represent the best of reach. Progressive Web Apps sit at the intersection of the capabilities of platform apps and the reach of web apps. A Progressive Web App includes features from both worlds.
Web
- Linkability
- Accessible by default
- Ubiquitous
- Easy to Deploy
- Easy to Update
- Everyone can publish
Platform apps
- Offline-capable
- High performance
- Device Integration
- Standalone experience
- Installed Icon
- Rich and reliable
{% Aside %} People tend to think of Progressive Web Apps as an app that the user can install from a browser instead of an app store. However, a PWA can be listed in many app stores today as an optional distribution channel, including Google Play Store (for Android and ChromeOS), Microsoft Store (for Windows 10 and 11), and Apple AppStore (for iOS, iPadOS, and macOS). For these cases, you must follow all store rules and requirements, but you will still get some of the advantages of a PWA. {% endAside %}
Adoption has its benefits
Hulu, a video streaming service in the USA, created a Progressive Web App version of their experience to replace their desktop apps which had poor user reviews and poor usage. As shared at Google I/O 2019, one developer could research and implement this experience from their existing web application in two weeks.
Within five months, 96% of their legacy app users had adopted the PWA, with a 27% increase in return visits and a 5.5% increase in engagement. Because it's in the launcher and on taskbars, PWAs are easier to return to than if they just lived in a tab.
JD.ID, an e-commerce platform in Indonesia providing delivery services for many products, wanted to expand its online presence by focusing on performance and a network-independent solid experience for their PWA. With this enhanced experience, they increased their overall mobile conversion rate by 53%, 200% for installed users, and increased their daily active users by 26%.
Clipchamp is an in-browser, desktop-class online video editor that empowers anyone to tell stories worth sharing through video. They saw 9% higher user retention with their PWA versus their standard desktop app users and have seen their PWA installations increasing at a rate of 97% each month in its first five months launched.
Corel Corporation's Gravit Designer is a powerful, desktop-class vector design tool that serves tens of thousands of daily active users demanding rich, affordable, accessible vector illustration software. Since adding a PWA as an install option for users, they've seen PWA users are 24% more active, the PWA accounts for 31% more repeat users, and PWA users are 2.5 times more likely to purchase Gravit Designer PRO, as compared to their other platforms and install options.
{% Aside %} Many other companies have implemented PWA and seen a benefit. Large companies have already published PWAs on various products, including Apple (AppStore Connect, Feedback Assistant), Microsoft (Office 365, Windows 365), Google (Duo, YouTube Music, Drive), Amazon (Luna), Facebook (Instagram Lite, Gaming). {% endAside %}
The streaming game changer
A great example of the power of Progressive Web Apps is the industry of streaming platforms, including cloud gaming and remote computing. Since 2021, most cloud game providers have launched Progressive Web Apps, letting you play console games from any device and just a browser or a PWA installation: iPhone, Android, iPad, laptops, Macs, or PCs. Amazon Luna, Microsoft Xbox Cloud Gaming, Facebook Gaming, Google Stadia, Nvidia GeForce Now, and BlueStacks X offer cloud gaming solutions over the browser as PWAs. They all provide a great experience with performance close to native on all platforms thanks to web technologies such as WebRTC, WebAssembly, and GamePad APIs.
Challenges
Having covered the advantages of using the web platform to publish PWAs, it's also important to be aware of the challenges you may face.
Cross-browser compatibility
Apple is a crucial company for the multi-device world, owning iOS, iPadOS, macOS, and Safari. While Apple has never used the term PWA in public, they've been supporting the technologies to make a PWA installable and offline-capable since 2018 on Safari for iPhones and iPads.
However, Apple's implementation of the PWA specs misses many features possessed by other browsers, in particular browsers powered by the Chromium engine.
In the middle, we also have Firefox and its Gecko engine with implementations including more PWA specs on Android, and fewer installation capabilities on desktop.
Limitations include the lack of push notifications, integration APIs (such as Web Bluetooth or WebNFC), and installation promotion techniques that help users know they can install the current website to get an app experience. In addition, there are several bugs with implemented features.
As with all web development, testing your experience on every platform is mandatory when releasing your PWA, and when a major new browser or OS version is released. You should always provide fallback solutions or alternative experiences when a feature is not available.
Awareness of PWAs
As a PWA developer, you will probably encounter an awareness problem, both on the business and user sides. Some business owners won't know about PWAs or will have misconceptions about the power and challenges of Progressive Web Apps.
When you publish a PWA, your next challenge is ensuring users understand that the website is installable, leading to an installed app experience.
The installation challenge is more significant on some platforms, such as iOS and iPadOS, and sometimes UX designers include screens that explain to the user how to install the app.
Compatibility
You need to remember that a Progressive Web App is just a web app, so content and services are running on top of standard specs and protocols. Therefore, a PWA technically runs everywhere the web runs; you don't need the platform to be compatible with any "PWA spec."
However, when we are talking about PWA and compatibility, typically, we are thinking about the capabilities to cross the boundaries of the browser and online-only contexts: icon installation and offline support.
{% Aside %} A PWA should work everywhere, even when the icon installation or the offline support capabilities are unavailable. Always plan your PWA to work without capabilities by checking support and offering fallback solutions. {% endAside %}
On top of the classic web platform support, let's check the support for basic app functionality, such as icon installation and offline capabilities.
97 %
Offline-ready browsers
88 %
Web users can install a PWA
Data sourced from StatCounter and Can I Use.
Desktop and laptops
In a world of multifactor devices, it's challenging to know what a desktop device is anymore. Still, at least from an operating system point of view, these browsers and stores are compatible with PWA installation and offline capabilities:
Windows 10 and 11 : Google Chrome (from version 73), Microsoft Edge (from version 79), Microsoft Store
ChromeOS : built-in Chrome browser (from version 72), Play Store (from version 85)
macOS, Linux, and Windows 7 and 8.x : Google Chrome (from version 73), Microsoft Edge
In the following video the user installs a PWA from the browser on a desktop computer, and then accesses it like any other app with its standalone window.
{% Video src="video/RK2djpBgopg9kzCyJbUSjhEGmnw1/d0cR0VyUhy30DkXEgGOt.mp4", alt="the user can install a PWA from the browser on a desktop computer, and then the user can access it like any other app with its standalone window", autoplay="true", loop="true", muted="true" %}
{% Aside 'caution' %} On desktop, Safari and Firefox do not support PWA installation. They do support offline capabilities, but the experience will always start within the browser user interface. It may get fullscreen, but never a standalone window on desktop. {% endAside %}
Mobile devices
Talking about mobile phones and tablets, a Progressive Web App is installable with offline capabilities using the following browsers and app stores:
iOS and iPadOS : Safari (since iOS 11.3), AppStore (since iOS/iPadOS 14, with some limitations), mobile configuration for enterprise distribution.
Android : Firefox, Google Chrome, Samsung Internet, Microsoft Edge, Opera, Brave, Huawei Browser, Baidu, UCWeb, Play Store (from version 72 with Google Chrome installed, or browsers compatible with TWA), Galaxy Store, Managed Play iframe for enterprise distribution.
{% Aside 'caution' %} On iOS and iPadOS, PWAs are only installable if the user is using Safari. That means that users won't install PWAs if they use different browser apps, such as Google Chrome, Firefox, or Microsoft Edge. On both Android and iOS, users can't install PWAs from many in-app browsers, such as Facebook Mobile Browser, Instagram, Google Search App, or Gmail. {% endAside %}
In the following video the user installs a PWA from the browser on a mobile device using the browser dialog, and also using the Add to Home screen menu.
{% Video src="video/RK2djpBgopg9kzCyJbUSjhEGmnw1/Nj9yCzWOm6wWqpNN9Ojz.mp4", alt="the user can install a PWA from the browser on mobile devices on Android and iPhones, using the browser dialog or the 'Add to Home screen' menu.", autoplay="true", loop="true", muted="true" %}
Other devices
Some other small devices support PWAs, such as game consoles (Xbox with Microsoft Store) or XR devices (Microsoft Hololens, plans for Facebook's Oculus). However, the rest of the devices with a browser do not typically accept PWAs, including:
- Game consoles
- Smart TVs
- Smartwatches
- Cars
Your PWA will always work in the browser of all devices with their specific limitations. This ability to work in many devices, lets you create multi-device journeys, where the user can start a task in one device and complete it on another device, with data synced across them, with the exact same deployed app.
Resources
- Introduction to Progressive Web Apps on MDN
- Progressive Web Apps Compatibility list
- How Progressive Web Apps can drive business success
- Clipchamp's video editor PWA installs see a 97% monthly growth
- PWA users are 2.5x more likely to purchase Gravit Designer PRO
Service workers
Users expect apps to start on slow or flaky network connections, or even when offline. They expect the content they've most recently interacted with, such as media tracks or tickets and itineraries, to be available and usable. When a request isn't possible, they expect the app to tell them instead of silently failing or crashing. And users wish to do it all quickly. As we can see in this study Milliseconds make millions, even a 0.1 second improvement in load times can improve conversion by up to 10%. In summary: users expect PWAs to be reliable and that's why we have service workers.
Hello service workers
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/iKWO7c2WNobLt30VZx9C.png", alt="A service worker as a middleware proxy, running device-side, between your PWA and servers, which includes both your own servers and cross-domain servers.", width="800", height="354" %}
When an app requests a resource covered by the service worker's scope, including when a user is offline, the service worker intercepts the request, acting as a network proxy. It can then decide if it should serve the resource from the cache via the Cache Storage API, from the network as normally would happen without a service worker, or create it from a local algorithm. This lets you provide a similar experience to that provided by a platform app. It can even work entirely off line.
{% Aside 'gotchas' %} Not all browsers support service workers. Even when present your service worker won't be available on first load or while it's waiting to activate. Therefore, treat it as optional and do not require it for core functionality. {% endAside %}
Registering a service worker
Before a service worker takes control of your page, it must be registered for your PWA. That means the first time a user comes to your PWA, network requests will not go directly to your server because the service worker is not yet in control of your pages.
After checking if the browser supports the Service Worker API, your PWA can register a service worker. When loaded, the service worker sets up shop between your PWA and the network, intercepting requests and serving the corresponding responses.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register("/serviceworker.js");
}
{% Glitch 'learn-pwa-sw-registration' %}
{% Aside %}
There is only one service worker per PWA, but that doesn't mean you need to place the code only in one file. A service worker can include other files using importScripts in every browser or using ECMAScript module imports in some modern browsers.
{% endAside %}
Verify if a service worker is registered
To verify if a service worker is registered, use developer tools in your favorite browser.
In Firefox and Chromium-based browsers (Microsoft Edge, Google Chrome, or Samsung Internet):
- Open developer tools, then click the Application tab.
- In the left pane, select Service Workers.
- Check that the service worker's script URL appears with the status "Activated". (You'll learn what this status means in the lifecycle section in this chapter). On Firefox the status can be "Running" or "Stopped".
In Safari:
- Click the Develop menu then the Service Workers submenu.
- Check that an entry with the current origin appears in the submenu. It opens an inspector over the service worker's context.
{% Aside %} You can remotely inspect a phone or tablet from your desktop browser when you want to check service worker registration status on a mobile device. You will see details on how to do that in the Tools and Debug chapter {% endAside %}
Scope
The folder your service worker sits in determines its scope. A service worker that lives at example.com/my-pwa/sw.js can control any navigation at the my-pwa path or below, such as example.com/my-pwa/demos/. Service workers can only control items (pages, workers, collectively "clients") in their scope. Scope applies to browser tabs and PWA windows.
Only one service worker per scope is allowed. When active and running, only one instance is typically available no matter how many clients are in memory (such as PWA windows or browser tabs).
{% Aside 'warning' %} You should set the scope of your service worker as close to the root of your app as possible. This is the most common setup as it lets the service worker intercept all the requests related to your PWA. Don't put it inside, for instance, a JavaScript folder or have it loaded from a CDN. {% endAside %}
Safari has more complex scope management, known as partitions, affecting how scopes work if you have cross-domain iframes. To read more about WebKit's implementation, read their blog post.
Lifecycle
Service workers have a lifecycle that dictates how they are installed, this is separate from your PWA installation.
The service worker lifecycle starts with registering the service worker. The browser then attempts to download and parse the service worker file. If parsing succeeds, its install event is fired. The install event only fires once.
Service worker installation happens silently, without requiring user permission, even if the user doesn't install the PWA. The Service Worker API is even available on platforms that do not support PWA installation, such as Safari and Firefox on desktop devices.
{% Aside 'gotchas' %}
Service worker registration and installation, while related, are different events. Registration happens when a page requests a service worker by calling register() as described previously. Installation happens when a registered service worker exists, can be parsed as JavaScript, and doesn't throw any errors during its first execution.
{% endAside %}
After the installation, the service worker is not yet in control of its clients, including your PWA. It needs to be activated first. When the service worker is ready to control its clients, the activate event will fire. This doesn't mean, though, that the page that registered the service worker will be managed. By default, the service worker will not take control until the next time you navigate to that page, either due to reloading the page or re-opening the PWA.
You can listen for events in the service worker's global scope using the self object.
{% Label %}serviceworker.js{% endLabel %}
// This code executes in its own worker or thread
self.addEventListener("install", event => {
console.log("Service worker installed");
});
self.addEventListener("activate", event => {
console.log("Service worker activated");
});
Updating a service worker
Service workers get updated when the browser detects that the service worker currently controlling the client and the new (from your server) version of the same file are byte-different.
{% Aside 'warning' %} When updating your service worker, do so without renaming it. Do not even add file hashes to the filename. Otherwise, the browser will never get the new version of your service worker! {% endAside %}
After a successful installation, the new service worker will wait to activate until the existing (old) service worker no longer controls any clients. This state is called "waiting", and it's how the browser ensures that only one version of your service worker is running at a time. Refreshing a page or reopening the PWA won't make the new service worker take control. The user needs to close or navigate away from all tabs and windows using the current service worker and then navigate back. Only then will the new service worker to take control. Visit this service worker lifecycle article for more information.
Service worker lifespan
Once installed and registered, a service worker can manage all network requests within its scope. It runs on its own thread, with activation and termination controlled by the browser. This lets it work even before or after your PWA is open. While service workers run on their own thread, there is no guarantee that in-memory state will persist between runs of a service worker, so make sure anything you want to reuse on each run is available either in IndexedDB or some other persistent storage.
If not already running, a service worker will start whenever a network request in its scope is asked for, or when a triggering event, like periodic background sync or a push message, is received.
Service workers don't live indefinitely. While exact timings differ between browsers, service workers will be terminated if they've been idle for a few seconds, or if they've been busy for too long. If a service worker has been terminated and an event occurs that would start it up, it will restart.
Capabilities
With a registered and active service worker, you have a thread with a completely different execution lifecycle than the main one on your PWA. However, by default, the service worker file itself has no behavior. It won't cache or serve any resources, as this has to be done by your code. You'll find out how in the following chapters.
Service worker's capabilities are not just for proxy or serving HTTP requests; other features are available on top of it for other purposes, such as background code execution, web push notifications, and process payments. We'll discuss these additions in the capabilities chapter.
Resources
- Service Worker API (MDN)
- Service Worker mindset
- WebKit Workers at your service
- ES Modules in Service Workers
- Service worker lifecycle
Serving
A key aspects of Progressive Web Apps is that they're reliable; they can load assets quickly, keeping users engaged and providing feedback immediately, even under poor network conditions. How is that possible? Thanks to the service worker fetch event.
The fetch event
{% BrowserCompat 'api.FetchEvent' %}
The fetch event lets us intercept every network request made by the PWA in the service worker's scope, for both same-origin and cross-origin requests. In addition to navigation and asset requests, fetching from an installed service worker allows page visits after a site's first load to be rendered without network calls.
The fetch handler receives all requests from an app, including URLs and HTTP headers, and lets the app developer decide how to process them.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/WExZeP8vV28bPDBlpxuG.png", alt="The service worker sits between the client and the network.", width="800", height="439" %}
Your service worker can forward a request to the network, respond with a previously cached response, or create a new response. The choice is yours. Here's a simple example:
self.addEventListener("fetch", event => {
console.log(`URL requested: ${event.request.url}`);
});
{% Aside 'caution' %}
While you can have more than one fetch event handler per service worker, only one can respond per request. For example, you can have different handlers that will act on different URL patterns. They are executed in the order they were registered until one of them calls respondWith().
{% endAside %}
Responding to a request
When a request comes into your service worker, there are two things you can do; you can ignore it, which lets it go to the network, or you can respond to it. Responding to requests from within your service worker is how you're able to choose what, and how it gets returned to your PWA, even when the user is offline.
To respond to an incoming request, call event.respondWith() from within a fetch event handler, like this:
// fetch event handler in your service worker file
self.addEventListener("fetch", event => {
const response = .... // a response or a Promise of response
event.respondWith(response);
});
You must call respondWith() synchronously and you must return a Response object. But you can't call respondWith() after the fetch event handler has finished, like within an async call. If you need to wait for the complete response, you can pass a Promise to respondWith() that resolves with a Response.
Creating responses
Thanks to the Fetch API, you can create HTTP responses in your JavaScript code, and those responses can be cached using the Cache Storage API and returned as if they were coming from a web server.
To create a response, create a new Response object, setting its body and options such as status and headers:
const simpleResponse = new Response("Body of the HTTP response");
const options = {
status: 200,
headers: {
'Content-type': 'text/html'
}
};
const htmlResponse = new Response("<b>HTML</b> content", options)
{% Aside 'warning' %} Be extra careful when creating responses with a service worker buggy code can lead to corner cases and hard to debug issues, such as the browser being unable to load the HTML document that renders your PWA. While debugging, you can always use your browser's reload button with the Shift key pressed, which will bypass the service worker; or use service worker devtools on supported browsers as we'll see in Tools and Debug. {% endAside %}
{% Glitch 'learn-pwa-serving-synthesize' %}
Responding from the cache
Now that you know how to serve HTTP responses from a service worker, it's time to use the Caching Storage interface to store assets on the device.
You can use the cache storage API to check if the request received from the PWA is available in the cache, and if it is, respond to respondWith() with it.
To do that, you first need to search within the cache. The match() function, available at the top-level caches interface searches all stores in your origin, or on a single open cache object.
The match() function receives an HTTP request or a URL as an argument and it returns a promise that resolves with the Response associated with the corresponding key.
// Global search on all caches in the current origin
caches.match(urlOrRequest).then(response => {
console.log(response ? response : "It's not in the cache");
});
// Cache-specific search
caches.open("pwa-assets").then(cache => {
cache.match(urlOrRequest).then(response) {
console.log(response ? response : "It's not in the cache");
}
});
{% Aside 'gotchas' %}
When using match() with respondWith(), the match promise will resolve either with the found item or with nothing. It will only reject if there was a problem that prevented the search operation from completing. To throw an error if no results are found, you'll need to inspect the results before returning a response to respondWith().
{% endAside %}
Caching strategies
Serving files only from the browser cache doesn't fit every use case. For example the user or the browser can evict the cache. That's why you should define your own strategies for delivering assets for your PWA.
You're not restricted to one caching strategy. You can define different ones for different URL patterns. For example, you can have one strategy for the minimum UI assets, another for API calls, and a third for image and data URLs.
To do this, read event.request.url in ServiceWorkerGlobalScope.onfetch and parse it through regular expressions or a URL Pattern. (At the time of writing, URL Pattern is not supported on all platforms).
The most common strategies are:
Cache First : Searches for a cached response first and falls back to the network if one isn't found.
Network First : Requests a response from the network first and if none is returned, checks for response in the cache.
Stale While Revalidate : Serves a response from the cache, while in the background requests the latest version and saves it to the cache for the next time the asset is requested.
Network-Only : Always replies with a response from the network or errors out. The cache is never consulted.
Cache-Only : Always replies with a response from the cache or errors out. The network will never be consulted. The assets that will be served using this strategy must be added to the cache before they are requested.
Cache first
Using this strategy, the service worker looks for the matching request in the cache and returns the corresponding Response if it's cached. Otherwise it retrieves the response from the network (optionally, updating the cache for future calls). If there is neither a cache response nor a network response, the request will error. Since serving assets without going to the network tends to be faster, this strategy prioritizes performance over freshness.
{% Aside %}
Remember the cache storage interface won't return a Response object. The reference will be undefined, if the request is not cached.
{% endAside %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/ki9e4T6ik7mfrN67Zysx.png", alt="The Cache First strategy", width="800", height="439" %}
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
// It can update the cache to serve updated content on the next request
return cachedResponse || fetch(event.request);
}
)
)
});
{% Glitch 'learn-pwa-serving-offline' %}
Network first
This strategy is the mirror of the Cache First strategy; it checks if the request can be fulfilled from the network and, if it can't, tries to retrieve it from the cache. Like cache first. If there is neither a network response nor a cache response, the request will error. Getting the response from the network is usually slower than getting it from the cache, this strategy prioritizes updated content instead of performance.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/bGCR5LiMRlt9urRcpnk4.png", alt="The Network First strategy", width="800", height="439" %}
self.addEventListener("fetch", event => {
event.respondWith(
fetch(event.request)
.catch(error => {
return caches.match(event.request) ;
})
);
});
Stale while revalidate
The stale while revalidate strategy returns a cached response immediately, then checks the network for an update, replacing the cached response if one is found. This strategy always makes a network request, because even if a cached resource is found, it will try to update what was in the cache with what was received from the network, to use the updated version in the next request. This strategy, therefore, provides a way for you to benefit from the quick serving of the cache first strategy and update the cache in the background.
{% Aside %} With this strategy, the assets are updated in the background. This means your users won't get the new version of the assets until the next time that path is requested. At that time the strategy will again check if a new version exists and the cycle repeats. {% endAside %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/C7OcnPRHNdTLe2043JHG.png", alt="The stale while revalidate strategy", width="800", height="439" %}
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const networkFetch = fetch(event.request).then(response => {
// update the cache with a clone of the network response
const responseClone = response.clone()
caches.open(url.searchParams.get('name')).then(cache => {
cache.put(event.request, responseClone)
})
return response
}).catch(function (reason) {
console.error('ServiceWorker fetch failed: ', reason)
})
// prioritize cached response over network
return cachedResponse || networkFetch
}
)
)
})
{% Aside 'caution' %}
The Response's body is a ReadableStream that can only be consumed once. That means that if you call fetch() to deliver the response to the PWA because the asset was not cached, you cannot reuse the same response for updating it in the cache. You need to call Response.clone() to use it twice.
{% endAside %}
{% Glitch 'learn-pwa-serving-stale-while-revalidate' %}
Network only
The network only strategy is similar to how browsers behave without a service worker or the Cache Storage API. Requests will only return a resource if it can be fetched from the network. This is often useful for resources like online-only API requests.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/NGxIKl6Lq8TY9GqXT8r8.png", alt="The Network only strategy", width="800", height="439" %}
Cache only
The cache only strategy ensures that requests never go to the network; all incoming requests are responded to with a pre-populated cache item. The following code uses the fetch event handler with the match method of the cache storage to respond cache only:
self.addEventListener("fetch", event => {
event.respondWith(caches.match(event.request));
});
{% Aside %}
With the cache only strategy you cache your assets before you need them, usually in the service worker's install event handler. Because this strategy never goes to the network, these assets will get updated only when you update your service worker and a new install event is fired.
{% endAside %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/rDI3LvI9y0SBUFfRCdgm.png", alt="Cache only strategy.", width="800", height="439" %}
{% Glitch 'learn-pwa-serving-cache-only' %}
Custom strategies
While the above are common caching strategies, you are in charge of your service worker and how requests are handled. If none of these work for your needs, create your own.
You can, for example, use a network first strategy with a timeout to prioritize updated content, but only if the response appears within a threshold you set. You can also merge a cached response with a network response and build a complex response from the service worker.
Updating assets
Keeping your PWA's cached assets up-to-date can be a challenge. While the stale while revalidate strategy is one way to do so, it's not the only one. In the Update chapter you will learn different techniques to keep your app's content and assets updated.
Resources
Tools and debug
All the tools available for web development are also helpful for progressive web app development, including libraries, frameworks, code editors, builders, developer tools, debuggers, and more. But when working with PWA-specific abilities, such as installability, service workers, offline support, and more, certain tools for PWA are extra helpful. Let's see them in action.
Simulators and devices
As mentioned in the Foundations chapter, you should use agnostic design patterns to offer the best experience to every user in every context. However, it's good practice to test your experiences on different devices.
{% Aside 'caution' %} Installed PWAs are new experiences for web developers to test and debug, but keep in mind that not every tool is compatible with this mode. {% endAside %}
You probably won't own dozens of physical devices, including iPhones, Android phones, tablets, and desktops or laptops with different operating systems. That's why simulators and emulators exist.
Simple simulators
Most developer tools within browsers let you test your PWAs in different screen sizes or network conditions while using a single desktop browser engine. Some of these tools can also force a different user agent under these simulations.
Some available simulators are:
- Chromium DevTools: Device Mode, network throttling and several sensor simulations available
- Firefox Developer Tools: Responsive Design Mode
- Safari Web Inspector: Responsive Design Mode
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/gnIHqhnkBZcz1loMEcMG.png", alt="Chromium DevTools simulating mobile devices.", width="800", height="513" %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/fheSiDVxegbRD6oxERQ1.png", alt="Firefox DevTools simulating mobile devices.", width="800", height="629" %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/Opi0TXLgyl6v6PfQ06MM.png", alt="Safari Web Inspector Responsive Design mode.", width="800", height="810" %}
Some commercial and free solutions are optimized for developers and designers, such as the open source Responsively app.
{% Aside 'gotchas' %}
Simple simulators can't simulate and test installation-related abilities, such as
display-moderelated media queries, iOS web app meta tags, app shortcuts, or web share target. {% endAside %}
Apple simulators
Apple offers the Simulator app (formerly known as iOS Simulator) which allows you to test your web app on different iPhones and iPads on various operating system versions.
The Simulator app is only available for macOS computers, and it comes with [Xcode, available in AppStore; it simulates iOS and iPadOS with different device configurations. It includes the real Mobile Safari app and the Web.app engine used when your PWA is installed on the home screen, so the final experience you see is fairly representative of an actual device.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/aa4SAqNofV2d1nw3btVF.png", alt="Simulator with Safari rendering websites on iPhones and iPad.", width="800", height="376" %}
To launch the app, once you have installed Xcode, you can open the Simulator in the Xcode menu, by selecting Open Developer Tools then Simulator. Once in the simulator, you can open Safari as if you are in a real iPhone or iPad. You can open other devices by selecting File then Open Simulator menu.
Some useful shortcuts for web developers while using Simulator to test PWAs are:
Command-Shift-H: go to the Home screen.Control-Command-Shift-H: access the App Switcher.Command-RightandCommand-Left: rotate the device.
Simulator is not a virtual machine, but an app running on top of your macOS that looks like an iPhone or iPad, so it doesn't have its own TCP stack. Therefore, if you use localhost within Simulator, Safari will point to your macOS localhost device.
By default, Xcode only installs the latest version of iOS, but you can go to Xcode, use the Preferences menu, and download older Simulators from the Components tab.
It's good practice to test your PWA on the latest version of iOS, the previous minor version, and at least one previous major version.
Android emulators
The Android ecosystem offers different emulators, but the ones available within the Android SDK are the most commonly used.
As a PWA developer, you also need browsers in your Android emulator, which will add a layer of complexity to testing, because AOSP (Android Open Source Project) doesn't include Google Chrome or Play Store to download browsers. Therefore, not every Android emulator is helpful for PWA testing.
The Android SDK comes with two tools useful for emulation:
- SDK Manager: downloads and updates different operating system versions and plugins.
- AVD Manager: adds, edits, and deletes Android Virtual Devices (AVDs), each of them representing one device with one Android OS installed, similar to a virtual machine instance.
You can only install the Android SDK or Android Studio, a free IDE including the Android SDK with the emulators. With the SDK, you will have to use the command line to open and set up your emulators. With Android Studio, you can open the tools needed from the Welcome screen's menu. Once in the AVD Manager, you can create as many devices as you want, with different combinations of screen size, abilities, and Android OS version.
{% Aside 'caution' %} Each Android Virtual Device can take more than 5GB of your hard drive. Be mindful of that when creating images. {% endAside %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/QQH4XtxtnRe7UmgCG47D.png", alt="The AVD manager with an Android Virtual device browsing a website", width="800", height="569" %}
{% Aside 'caution' %}
Android devices are virtual machines on top of your host computers, so they have their own TCP stacks. Therefore, localhost in an Android browser within an AVD won't be the same as your localhost in your host desktop computer. Later in this chapter, we will see how to solve this restriction with port forwarding.
{% endAside %}
With an Android emulator, you can check your PWA installation support, the entire user experience, and if the abilities you are using are working as expected.
Using Google Chrome
To use Google Chrome in the Android emulator, you have to create an AVD with Play Services inside. To do that, make sure the SDK you use for your AVD has the Play icon, as you can see in the following image:
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/KNBWJ3Nu8SYFbh8KemT9.png", alt="Creating AVDs with an operating system including Play services.", width="800", height="560" %}
AVDs with Play services also include Play Store, so you can update your Chrome with the latest version after setting up your account with your Google account.
Using other browsers
If you use an AVD with Play services, you can also download browsers from the Play Store.
Most of the browsers available for Android, including Samsung Internet, Microsoft Edge, Opera, Firefox, and Brave, are available as APKs (Android Packages). If you have the APK for the browser you want to test, you can just drag the file to the emulator or install it through the command-line using ADB.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/Ngc9KZO0UdLiGYF2TGt0.png", alt="Android emulator installing Microsoft Edge from Play store.", width="780", height="746" %}
Desktop emulators
Emulating a different desktop computer is typically done via a virtual machine system, such as VirtualBox or VMWare. Even with these tools, emulating some environments is impossible, such as emulating macOS on Windows or Linux, and some other options may require a license, such as emulating Windows on macOS or Windows.
Using physical devices
Using actual devices to test your PWA is also a good idea. We don't need to own several devices as there are some cloud-based solutions where you can use physical devices remotely. There are some free solutions and some commercial solutions with a free-tier available.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/R6nJTg2Uoh5EstlZdr21.png", alt="Remote Test Lab from Samsung with a foldable phone.", width="800", height="460" %}
One of the available solutions is Samsung Remote Test Lab, a free solution to test your PWA on Samsung devices, including phones, tablets, and foldable devices.
{% Aside 'gotchas' %} Some cloud-based device lab solutions are suitable for device-specific app testing. They are not optimized for PWAs, so you can only use them if you plan to create a package to distribute your PWA in an app store. {% endAside %}
Remote inspection
When you want to debug your Progressive Web App in an actual device, a simulator, or an emulator, you may want to connect a remote inspection session with your desktop's browser tools.
There are commercial tools available, but all the browsers also offer ways to do it, including:
- WebKit Remote Inspector to connect to Safari and installed PWAs on iOS- and iPadOS-connected devices and simulators.
- Chromium DevTools Remote Debugging to connect to Chrome, Edge, Samsung Internet, and other Chromium-based browsers on Android and PWAs installed from those browsers, including connected physical devices and emulators.
- Firefox Remote Debugging to connect to Firefox for Android on connected actual devices and emulators.
Port forwarding for Android
When testing PWAs available on localhost on Android physical devices or emulators, you will have a problem, as localhost will point to the Android TCP stack and not your development machine.
You may want to use your local IP address as a replacement to localhost, but that's not a good idea as service workers and many abilities will only work with HTTPS connections, with the exception of localhost, so your PWA won't work offline or pass PWA criteria.
You can solve the problem by enabling port forwarding on a Chromium browser on your desktop computer. In that case, you can bridge a port on localhost on the Android device to any origin and port from your host computer, including your development computer's localhost. Check this guide for more information.
Chromium browsers
Chromium browsers offer many tools for debugging and testing Progressive Web Apps, starting from DevTools.
Most Chromium-based browsers, including Samsung Internet, Microsoft Edge, and Google Chrome, offer different channels, such as Stable, Beta, and Canary. You can install separate versions on desktop and Android to test your PWA on future versions of the browser. This allows you to build and test features that are not yet widely available, or to test deprecations and changes, and work out how your app will behave in newer versions.
Using remote inspection, you can use all these tools to debug and test your PWA on desktop and Android devices.
Service worker tools
Chromium DevTools has a complete set of tools to debug service workers and their APIs' "Application" tab. From the "Service Workers" section, you can:
- See service workers' installation status and lifecycle.
- Update and unregister the service worker.
- Follow the update cycle.
- See current service workers' clients.
- Send a push message to a service worker.
- Register Background Sync and Periodic Background Sync operations.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/WJ0fBXggrFnhifcAIcBb.png", alt="Service Worker debugging tools con Chromium DevTools.", width="800", height="491" %}
Storage tools
Within Application then Storage, you can see, preview, update, and delete data from your origin, such as Web Storage entries or IndexedDB stores.
Inside Application, Cache then Cache Storage, you can see all the caches stored in current origin, preview content, and delete entries. Read more about the cache tool.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/WJ0fBXggrFnhifcAIcBb.png", alt="Cache Inspector in Chromium DevTools", width="800", height="491" %}
Also, selecting from Application then Storage, you can see the current quota used, simulate custom quota storage, and clear all your data, including the service worker registration, using Clear site data.
Background services
Chromium DevTools also has a set of background services event recording tools, found by clicking Application then Background Services. This lets you see what happens with some events in the background on top of the service workers' API. Read more about these tools.
Web app manifest tools
Chromium DevTools has a section for Web App Manifest and installability criteria under Application, Manifest. In this section, you can check whether the manifest has loaded correctly, the manifest's values, how the icons look, the app id, and a quick checker for maskable icons.
Warnings and errors around installability criteria issues are also shown in this section.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/uBOt9BBbF15lX7Cxju6t.png", alt="Debugging Web App Manifest in Chromium DevTools.", width="800", height="713" %}
{% Aside %}
Chromium-based browsers have some flags that may be useful while testing PWAs. Check all the available options by browsing to about://flags/
{% endAside %}
Installation debugging
On Android devices using WebAPK installation mode, you can access a list of installed apps by browsing to about:webapk on Chromium browsers.
You will see the current update status and request a WebAPK update from here.
On desktop computers, you can see a user-friendly list of installed PWAs by browsing to about:apps, and a debug version by browsing to about:app-service-internals.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/exwzkevoGROml8uH5ZxL.png", alt="Google Chrome WebAPK debug screen on Android.", width="800", height="400" %}
Safari
At the time of writing, Safari has a more limited set of tools available for PWA testing and debugging. There are no tools to see and debug the state and lifecycle of service workers, no inspector for the cache's content, and no tools for web app manifest support and installability on iOS and iPadOS.
{% Aside 'gotchas' %} You can only inspect and debug PWAs on iOS and iPadOS if you have a macOS computer with an updated OS using Safari desktop or Safari Technology Preview. There are some commercial tools such as Inspect that allow you to inspect web apps on iOS and iPadOS from other operating systems. {% endAside %}
Safari is available only on stable, while Safari Technology Preview available for macOS will only let you try abilities of future versions of Safari beforehand. The iOS and iPadOS beta programs sometimes include new versions of Safari that you can use for testing.
Service worker tools
What you can do on Safari (both on macOS and remotely for iOS and iPadOS) is to open an inspector window for a service worker that is currently running.
From Safari on macOS, the Develop, Service Workers menu will list all the currently running service worker sessions. You have to select the device to inspect in the Develop menu for remote inspection. Service workers' contexts will appear in the same submenu as window contexts under the origin's or PWA's installed name.
If you select one of them, Safari will open a new window with a restricted inspector, including only Consoles, Sources, and Network Tabs.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/jho0nKOzPq9XPH8g24Hk.png", alt="A Service Worker Web Inspector from a PWA in Safari.", width="800", height="427" %}
{% Aside %} Service Worker Detector is a free Safari extension available in AppStore that will let you see service worker registration status and the cache storages available under the current origin, abilities which are not available on WebKit's Inspector. {% endAside %}
Firefox
Firefox supports service workers on all platforms and App Manifest for installation only on Android. You can access the tools for PWAs on desktop and Android by a USB remote inspection session.
You can use the desktop version known as Firefox Developer Edition. As with Chromium browser, Firefox has versions in different channels on desktop and Android, including stable, beta, and dev versions.
Tools for PWAs
The service worker inspector on Firefox is a basic tool available in Developer Tools under Application, Service Workers. It lets you see the currently registered service worker, check its running status, and unregister it. Debugging the service worker's code may only be available on some developer versions of Firefox.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/AihFdYbhvY7mpqWVQCuH.png", alt="Firefox Developer Tools for Service Worker and Manifest.", width="800", height="454" %}
The manifest tool is available under Application, Manifest, and it only renders the manifest's values with a preview of the icons.
Under Storage, you can manage the origin's storage, including IndexedDB and Cache Storage.
You can read more about Firefox developer tooling for web apps.
{% Aside %} To develop your PWA, you can use any code editor, IDE, and toolchain of your choice. If you are using Visual Studio code as your code editor, you may want to check the PWA Studio free extension from the PWABuilder team. This has many tools for PWA developers such as an icon resizer, a service worker generator, and a quick way to create PWAs launchers–useful for app store distribution. {% endAside %}
Resources
- Chrome DevTools: Debug Progressive Web Apps
- Debug PWAs on Edge
- Firefox Developer Tools: Application Tab
- Debugging Service Workers in Chrome (video)
Update
You have published your PWA: some users use it from the browser, others install it on their devices. When you update the app, it's important to apply best practices to avoid pitfalls.
You may update:
- App data.
- Assets already cached in devices.
- The service worker file, or its dependencies.
- Manifest metadata.
Let's check out the best practices for each of these elements.
Updating data
To update data, such as that stored in IndexedDB, you can use tools such as Fetch, WebRTC, or WebSockets API. If your app supports any offline features, remember to keep the data that supports the features updated too.
On compatible browsers, there are options to sync data, not only when the user opens the PWA but also in the background. These options are:
- Background synchronization: saves requests that failed and retries them using sync from the service worker.
- Web periodic background sync: syncs data periodically in the background, at specific times, allowing the app to provide updated data even if the user hasn't opened the app yet.
- Background Fetch: downloads large files, even when the PWA is closed.
- Web push: sends a message from the server that wakes up the service worker and notifies the user. This is commonly called a 'push notification'. This API requires the user's permission.
All these APIs are executed from the service worker context. They are currently available only on Chromium-based browsers, on Android, and desktop operating systems. When you use one of these APIs, you can run code in the service worker thread; for example, to download data from your server and update your IndexedDB data.
{% Aside %}
The Web Push API is the only API to notify the user from the background and its push message can include a small amount of data: it can carry a payload of up to around 2KB.
{% endAside %}
Updating assets
Updating assets includes any changes to files you use to render the app's interface, such as HTML, CSS, JavaScript, and images. For example, a change in your app's logic, an image that is part of your interface, or a CSS stylesheet.
Update patterns
Here are some common patterns to handle app updates, but you can always customize the process to your own needs:
- Full update: every change, even a minor one, triggers the replacement of the entire cache content. This pattern mimics how device-specific apps handle updates, and it will consume more bandwidth and will take more time.
- Changed assets update: only the assets that have changed since the last update get replaced in cache. It is often implemented using a library such as Workbox. It involves creating a list of cached files, a hash representation of the file, and timestamps. With this information, the service worker compares this list with the cached assets and decides which assets to update.
- Individual assets update: each asset is updated individually when it changes. The stale while revalidate strategy described in the Serving chapter is an example of updating assets individually.
When to update
Another good practice is to find a good time to check for updates and apply them. Here are some options:
- When the service worker wakes up. There is no event to listen for this moment, but the browser will execute any code in the service worker's global scope when it wakes up.
- In the main window context of your PWA, after the browser loaded the page, to avoid making the app load slower.
- When background events are triggered, such as when your PWA receives a push notification or a background sync fires. You can update your cache then and your users will have the asset's new version the next time they open the app.
{% Aside 'warning' %} When checking for updates, make sure you don't interrupt the loading process and impact your app's performance. Do not wait for an update check before rendering the app as it will harm your user experience and reflect on your core web vitals. If you need to update part of your app frequently, use the stale while revalidate caching strategy. {% endAside %}
Live updates
You can also choose whether to apply an update when the app is open (live) or closed. With the app closed approach, even though the app has downloaded new assets, it won't make any changes and will use the new versions on the next load.
A live update means as soon as the asset is updated in the cache, your PWA replaces the asset in the current load. It is a complex task not covered in this course. Some tools that help to implement this update are livereload-js and CSS asset update CSSStyleSheet.replace() API.
Updating the service worker
The browser triggers an update algorithm when your service worker or its dependencies change. The browser detects updates using a byte-by-byte comparison between the cached files and the resources coming from the network.
Then the browser tries to install the new version of the service worker, and the new service worker will be in the waiting state, as described in the Service workers chapter. The new installation will run the install event for the new service worker. If you are caching assets in that event handler, assets will also be re-cached.
{% Aside 'caution' %} If you delete or rename your service worker file, the browser won't remove the previously registered service worker from its clients. To completely remove a service worker, you need to use the getRegistrations() and the unregister() methods. {% endAside %}
{% Aside %} Installing a new service worker version doesn't automatically remove previously cached assets. You should explicitly delete assets you no longer need. {% endAside %}
Detecting service worker changes
To detect that a new service worker is ready and installed, we use the updatefound event from the service worker registration. This event is fired when the new service worker starts installing. We need to wait for its state to change to installed with the statechange event; see the following:
async function detectSWUpdate() {
const registration = await navigator.serviceWorker.ready;
registration.addEventListener("updatefound", event => {
const newSW = registration.installing;
newSW.addEventListener("statechange", event => {
if (newSW.state == "installed") {
// New service worker is installed, but waiting activation
}
});
})
}
Force override
The new service worker will be installed but waiting for activation by default. The wait prevents the new service worker from taking over old clients that might not be compatible with the new version.
Even though it is not recommended, the new service worker can skip that waiting period and start the activation immediately.
{% Aside 'caution' %}
skipWaiting() means that your new service worker is probably controlling pages that were loaded with an older version. This means some of your page's fetches will have been handled by your old service worker, but your new service worker will be handling subsequent fetches. If this might prevent your app from working, don't use skipWaiting().
This warning comes from service worker lifecycle. Check out the article for more best practice advice.
{% endAside %}
self.addEventListener("install", event => {
// forces a service worker to activate immediately
self.skipWaiting();
});
self.addEventListener("activate", event => {
// when this SW becomes activated, we claim all the opened clients
// they can be standalone PWA windows or browser tabs
event.waitUntil(clients.claim());
});
The controllerchange event fires when the service worker controlling the current page changes. For example, a new worker has skipped waiting and become the new active worker.
navigator.serviceWorker.addEventListener("controllerchange", event => {
// The service worker controller has changed
});
Updating metadata
You can also update your app's metadata, which is mainly set in the web app manifest. For example, update its icon, name, or start URL, or you can add a new feature such as app shortcuts. But what happens with all the users who have already installed the app with the old icon on their devices? How and when do they get the updated version?
The answer depends on the platform. Let's cover the options available.
{% Aside 'gotchas' %}
The id property uniquely identifies your PWA; changing id means your PWA will be considered a completely new PWA. To avoid unexpected behavior, before you set or change the id property, read Uniquely identifying PWAs with the web app manifest id property.
{% endAside %}
Safari on iOS, iPadOS, and Android browsers
On these platforms, the only way to get the new manifest metadata is to re-install the app from the browser.
{% Aside %}
If you modify your start_url the browser may install the app as a completely new app. However, it has been a common practice to add a manual version identifier within the start_url property, such as /?version=3 to identify the version. From Chrome 96 you can add an id into the manifest to identify your app.
{% endAside %}
Google Chrome on Android with WebAPK
When the user has installed your PWA on Android using Google Chrome with WebAPK activated (most of the Chrome PWA installations), the update will be detected and applied based on an algorithm. Check out the details in this manifest updates article.
Some additional notes about the process:
If the user doesn't open your PWA, its WebAPK won't be updated. If the server doesn't respond with the manifest file (there is a 404 error), Chrome won't check for updates for a minimum of 30 days, even if the user opens the PWA.
Go to about:webapks in Chrome on Android to see the status of the "needing update" flag, and request an update. In the Tools and debug chapter, you can read more about this debugging tool.
Samsung Internet on Android with WebAPK
The process is similar to the Chrome version. In this case, if the PWA manifest requires an update, during the next 24 hours the WebAPK will be updated on Wi-Fi after minting the updated WebAPK.
Google Chrome and Microsoft Edge on desktop
On desktop devices, when the PWA is launched, the browser determines the last time it checked the local manifest for changes. If the manifest hasn't been reviewed since the browser last started or hasn't been checked in the last 24 hours, the browser will make a network request for the manifest and then compare it against the local copy.
When selected properties are updated, it will trigger an update after all windows have been closed.
Alerting the user
Some updating strategies need a reload or a new navigation from the clients. You'll want to let the user know there is an update waiting but give them the chance to update the page when it is best for them.
To inform the user, there are these options:
- Use the DOM or canvas API to render a notice on the screen.
- Use the Web Notifications API. This API is part of the push permission to generate a notification in the operating system. You will need to request web push permission to use it, even if you don't use the push messaging protocol from your server. This is the only option we have if the PWA is not opened.
- Use the Badging API to show that updates are available in the PWA's installed icon
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/s20eK3XJWqdxYvH4HwZV.png", alt="An update notification shown in the DOM.", width="800", height="821" %}.
More about the Badging API
The Badging API lets you mark your PWA's icon with a badge number, or a badge dot on compatible browsers. A badge dot is a tiny mark within the installed icon that expresses something is waiting inside the app.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/yicjcQZkdbS2sj732BEp.webp", alt="Example of Twitter with eight notifications and another app showing a flag type badge.", width="800", height="252" %}
You need to call setAppBadge(count) on the navigator object to set a badge number. You can do this from the window or service worker's context when you know there is an update to alert the user.
let unreadCount = 125;
navigator.setAppBadge(unreadCount);
To clear the badge, you call clearAppBadge() on the same object:
navigator.clearAppBadge();
{% Aside %} The Badging API works on a best-effort mechanism; the browser will do its best to match your request, but sometimes the browser will revert to a different approach. For example, a badge number is not available on some Android devices, so a dot badge is used, no matter the number you specify. {% endAside %}
Resources
- Service Worker Lifecycle
- How Chrome handles updates to the web app manifest
- Synchronize and update a PWA in the Background
- Richer offline experiences with the Periodic Background Sync API
- Push API
- Push notifications overview
- Badging for app icons
- Workbox 4: Implementing refresh-to-update-version flow using the workbox-window module
Web app manifest
The web app manifest is a file you create that tells the browser how you want your web content to display as an app in the operating system. The manifest can include basic information such as the app's name, icon, and theme color; advanced preferences, such as desired orientation and app shortcuts; and catalog metadata, such as screenshots.
{% Aside 'warning' %} A web app manifest is a required installability criteria in every browser. Your PWA will not install without it. {% endAside %}
Each PWA should include a single manifest per application, typically hosted in the root folder, and linked on all HTML pages your PWA can be installed from. Its official extension is .webmanifest, so you could name your manifest something like app.webmanifest.
{% Aside %}
While the recommended extension is .webmanifest, any filename will work as long as it's delivered with the application/manifest+json content type or another JSON-valid content type, such as text/json. As such, many PWAs, particularly older ones, use manifest.json instead.
{% endAside %}
Adding a web app manifest to your PWA
To create a web app manifest, first make a text file with a JSON object that contains at least a name field with a string value:
{% Label %}app.webmanifest:{% endLabel %}
{
"name": "My First Application"
}
But creating the file is not enough, the browser needs to know it exists, too.
Linking to your manifest
To make the browser aware of your web app manifest, you need to link it to your PWA using a <link> HTML element and the rel attribute set to manifest on all of your PWA's HTML pages. This is similar to how you link a CSS stylesheet to a document.
{% Label %}index.html:{% endLabel %}
<html lang="en">
<title>This is my first PWA</title>
<link rel="manifest" href="/app.webmanifest">
{% Aside 'caution' %} If your PWA includes multiple HTML pages, make sure to link its manifest to all of them. {% endAside %}
Debugging the manifest
To ensure the manifest is set up correctly, you can use Inspector in Firefox and DevTools in every Chromium-based browser.
{% Aside 'gotchas' %}
If you see a message saying that the browser detected no manifest and you've included a <link> to it correctly, check to make sure you can load the manifest in the browser and that its content type is correctly set. You can also try renaming it to manifest.json.
{% endAside %}
For Chromium browsers
In DevTools
- In the left pane, under Application, select Manifest.
- Check the fields of the manifest as parsed by the browser.
For Firefox
- Open the Inspector.
- Go to the Application tab.
- Select the Manifest option in the left panel.
- Check the fields of the manifest as parsed by the browser.
{% Aside 'caution' %} It's not possible to debug web app manifests in Safari, the only option you have to see if it's working is to try to install your PWA on iOS or iPadOS. {% endAside %}
Designing your PWA experience
With your PWA now connected to its manifest, it's time to fill out the rest of the fields to define the experience for your users.
{% Aside 'warning' %}
Before the web app manifest spec was defined, several browsers, including Safari on iOS/iPadOS and Chrome on Android, supported custom <meta> elements to describe the application experience, such as apple-mobile-web-app-capable. Do not use these <meta> elements today; it's no longer recommended, and may harm the installation experience when the browser can't load the manifest properly; the experience you get as a fallback may be different and unexpected.
{% endAside %}
Basic fields
The first set of fields represents the core information about your PWA. They are used to build the installed PWA's icon and window and determine how it starts up. They are:
name
: Full name of your PWA. It will appear along with the icon in the operating system's home screen, launcher, dock, or menu.
short_name
: Optional, a shorter name of your PWA, used when there is not enough room to display the full value of the name field. Keep it under 12 characters to minimize the possibility of truncation.
icons
: Array of icon objects with src, type, sizes, and optional purpose fields, describing what images should represent the PWA.
start_url
: The URL the PWA should load when the user starts it from the installed icon. An absolute path is recommended, so if your PWA's home page is the root of your site, you could set this to ‘/' to open it when your app starts. If you don't provide a start URL, the browser can use the URL the PWA was installed from as a start. It can be a deep link, such as the details of a product instead of your home screen.
display
: One of fullscreen, standalone, minimal-ui, or browser, describing how the OS should draw the PWA window. You can read more about the different display modes in the App Design chapter. Most use cases implement standalone.
id
: A string that uniquely identifies this PWA against others that may be hosted on the same origin. If it's not set, the start_url will be used as a fallback value. Keep in mind that by changing the start_url in the future (such as when changing a query string value) you may be removing the browser's ability to detect that a PWA is already installed.
{% Aside 'caution' %}
If you published a PWA before September 2021 read about the best practice for adding an id property now in Uniquely identifying PWAs with the web app manifest id property.
{% endAside %}
{% Aside 'caution' %} Some manifest fields are not supported by every browser. When a browser doesn't support a field, it's just ignored and your manifest is still valid. You can learn more about each field at Web App Manifests. {% endAside %}
Icons
Your PWA's icon is its visual identity across your users' devices when installed, so it's important to define at least one. Because the icons property is a collection of icon objects, you can define several icons in different formats to provide the best icon experience for your users. Each browser will pick one or more icons based on its needs and the operating system it's installed on, the icons closer to the specifications needed.
{% Aside 'caution' %} While PNGs can include transparency, providing transparent icons may cause unexpected issues on some operating systems. For example, iOS, iPadOS, and Android 8 and later fill in the transparency with a background color out of your control. For better compatibility, use a square non-transparent icon. {% endAside %}
If you need to pick only one icon size, it should be 512 by 512 pixels. However, providing more sizes is recommended including 192 by 192, 384 by 384, and 1024 by 1024 pixel-sized images, too.
"icons": [
{
"src": "icons/512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "icons/1024.png",
"type": "image/png",
"sizes": "1024x1024"
}
]
{% Aside 'caution' %} Some resources will suggest you add more icon sizes for Android, such as 48 by 48, 72 by 72, 96 by 96, 144 by 144, 168 by 168, 256 by 256. While you can add as many sizes as you want, these are no longer necessary as they are Android icon sizes for legacy devices; even if you still have some users expecting those resolutions, the browser will resize one of your shipped icons. {% endAside %}
If you don't provide an icon or the icons are not in the recommended sizes, on some platforms you won't pass installation criteria. On other platforms, the icon will be automatically generated, for instance from a screenshot of the PWA or by using a generic icon.
Maskable icons
Some operating systems, such as Android, adapt icons to different sizes and shapes. For example, on Android 12, different manufacturers or settings can change the shape of icons from circles to squares to rounded-corner squares. To support these kinds of adaptive icons, you can provide a maskable icon using the purpose field.
To do so, provide a square image file that has its main icon contained within a “safe zone”, a circle centered in the icon with a radius of 40 percent of the width of the icon. (See the image below.) Devices that support maskable icons will mask your icon as needed.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/TcH5a39fkg9wYN34V4Qg.png", alt="The safe area marked as a 40 percent radius centered circle within the square icon", width="800", height="533" %}
{% Aside 'caution' %} If you don't provide a maskable icon, devices that expect them may shrink your icon and make it into the shape they need with a white background. {% endAside %}
Here's an example of a maskable icon rendered in a number of commonly used shapes:
In the following image, if you use the icon at the left as a maskable icon, you will end up with poor results on devices when a shape mask is applied.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/kKuLmXu1wuztin157EeD.png", alt="An icon that is not suitable for a maskable icon.", width="800", height="533" %}
This image could be made usable with more padding.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/KVcK8eRlJBlvlfOt3BbG.png", alt="The icon with more padding is suitable for masks.", width="800", height="533" %}
Maskable icons should be 512 by 512 at least. With one created, you can add it to your icons collection to improve the experience for supported devices:
"icons": [
{
"src": "/icons/512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "/icons/1024.png",
"type": "image/png",
"sizes": "1024x1024"
},
{
"src": "/icons/512-maskable.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
},
]
In most cases, if your maskable icon isn't displaying well, you can improve it by adding more padding. Maskable.app is a free online tool to test and create a maskable version of your icon.
Recommended fields
The next set of fields to include are ones that will improve your user's experience, even though they're not required for installability.
theme_color
: Default color for the application, sometimes affecting how the OS displays the site (for instance, the window and title bar color on desktop, or the status bar color on mobile devices). This color can be overridden by the HTML theme-color <meta> element.
background_color
: Placeholder color to display in the application's background before its stylesheet is loaded. Safari on iOS and iPadOS and most desktop browsers currently ignore this field.
scope
: Changes the navigation scope of the PWA, allowing you to define what is and isn't displayed within the installed app's window. For example, if you link to a page outside of the scope, it will be rendered in an in-app browser instead of within your PWA window. This will not, however, change the scope of your service worker.
The next image shows how the theme_color field is used for the title bar on a desktop device when you install a PWA.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/KWRGFZ2yIb0KNSGG3fI0.png", alt="The same PWA installed on desktop with a different theme color.", width="800", height="533" %}
{% Aside %}
If you want to provide different theme colors for dark and light modes, you can override the manifest's value with a meta tag in the HTML using a media query, such as with <meta name="theme-color" content="red" media="(prefers-color-scheme: dark)">
{% endAside %}
When defining colors in the manifest, such as within theme_color and background_color, you should use CSS named colors, such as salmon or orange, RGB colors such as #FF5500, or color functions without transparency such as rgb() or hsl(). Check the App design chapter for more information.
{% Aside 'warning' %}
Do not use transparency, CSS variables, gradient functions, or color functions with transparency (such as rgba()) as they are not supported by most browsers. You will get inconsistent results.
{% endAside %}
Try it
{% Glitch 'learn-pwa-web-app-manifest' %}
Splash screens
On some devices, a static image is rendered while your PWA is being loaded to provide immediate feedback to the user.
Android uses the theme_color, background_color, and icon values to generate the splash screen.
When you install a PWA on Android, the device will generate a splash screen with the information that comes from your manifest as seen in the following diagram.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/uO7fu1MHNzfipVZnbS7H.png", alt="A PWA on Android splash screen taking different values from the manifest.", width="800", height="533" %}
Safari on iOS and iPadOS, on the other hand, doesn't use the web app manifest to generate splash screens. Instead, they use an image linked from a proprietary <link> element similar to how they handle icons. Check the Enhancement chapter for more details.
Extended fields
The next set of fields offers additional information about your PWA. They are all optional.
lang
: A language tag specifying the primary language of the manifest's values, such as en for English, pt-BR for Brazilian Portuguese, or in for Hindi.
dir
: The direction to display direction-capable manifest fields (such as name, short_name, and description). Valid values are auto, ltr (left-to-right), and rtl (right-to-left).
orientation
: Desired orientation for the app once installed. A game may set this to request a landscape-only orientation. Several values are accepted, but if included it's typically portrait or landscape explicitly.
{% Aside %} The Web Application Manifest spec doesn't allow internationalization within the file to localize fields' values and images. To support multilingual PWAs with different web app manifest values you need to implement a custom solution to change those values per language. Watch 5 tips for your PWA for more ideas. {% endAside %}
Promotional fields
The fourth set of fields lets you provide promotional information about your PWA, for instance, in install flows, listings, and search results.
description
: An explanation of what the PWA does.
screenshots
: Array of screenshot objects with src, type, and sizes (similar to the icons object) intended to showcase the PWA. There are no size restrictions.
categories
: Array of categories the PWA should belong to be used as hints for listings, optionally from the list of known categories]. These values are typically lowercase.
iarc_rating_id
: The International Age Rating Coalition certification code for the PWA, if you have one. It is intended to be used to determine which ages your PWA is appropriate for.
{% Aside 'caution' %} Promotional fields have been moved to their own spec: Web App Manifest - Application Information. Check that spec for the latest additions to these fields. {% endAside %}
You can see these promotional fields in action today. On Android, for example, if your PWA is installable and you provide values for at least the description and screenshots fields, the installation dialog experience transforms from a simple "Add to the home screen" info bar, to a richer installation dialog similar to the one from an app store.
On Android, you can get a nicer installation UI if you provide values for the promotional fields, as you can see in the next video
{% Video src="video/RK2djpBgopg9kzCyJbUSjhEGmnw1/eU7EiYjZKtVsBrQ3xfBz.mp4" %}
See these promotional fields in action:
{% Glitch 'learn-pwa-web-app-manifest-promotional' %}
Capabilities Fields
Finally, there are a number of fields related to different capabilities that your PWA can use in supported browsers, such as the shortcuts, share_target, display_overrides fields as we cover in the Capabilities chapter. There are also fields, like related_apps and prefer_related_apps (see the Detection chapter for more information), to connect your PWA to installed apps, often from an app store.
Many new fields may appear in the future while browsers add more capabilities to Progressive Web Apps.
Resources
- Add a Web App Manifest
- Adaptive icon support in PWAs with maskable icons
- Richer PWA installation UI
- MDN: Web App Manifest
Window management
Running on your own window, managed by your PWA, has all the advantages and responsibilities of any window in that operating system, such as:
- The ability to resize and move the window around on multi-window operating systems, like Windows or ChromeOS.
- Sharing the screen with other app windows, as in iPadOS split mode or Android split-screen mode.
- Appearing in docks, taskbars, and in the alt-tab menu on desktop, and multi-task window lists on mobile devices.
- The ability to minimize, move the window across screens and desktops, and close it at any time.
Moving and resizing the window
Your PWA window can be of any size and positioned anywhere on the screen on desktop operating systems. By default, when the user opens the PWA for the first time after installation, the PWA gets a default window size of a percentage of the current screen, with a maximum resolution of 1920x1080 positioned at the top-left corner of the screen.
The user can move and resize the window, and the browser will remember the last preference, so the next time the user opens the app, the window will retain the size and position from the previous usage.
There is no way to define your PWA's preferred size and position within the manifest. You can only reposition and resize the window using the JavaScript API. From your code, you can move and resize your own PWA window using the moveTo(x, y) and resizeTo(x, y) functions of the window object.
For example, you can resize and move your PWA window when the PWA loads using:
document.addEventListener("DOMContentLoaded", event => {
// we can move only if we are not in a browser's tab
isBrowser = matchMedia("(display-mode: browser)").matches;
if (!isBrowser) {
window.moveTo(16, 16);
window.resizeTo(800, 600);
}
});
You can query the current screen size and position using the window.screen object; you can detect when the window is resized using the resize event from the window object. There is no event for capturing the window move, so your option is to query the position frequently.
{% Aside %}
Instead of moving and resizing the window absolutely, you can move relatively and resize using moveBy() and resizeBy().
{% endAside %}
{% Aside %} On mobile devices, moving or resizing your window won't make any changes on the screen. {% endAside %}
Browsing to other sites
If you want to send the user to an external site that is out of the scope of your PWA, you can do so with a standard <a href> HTML element, using location.href or opening a new window on compatible platforms.
Currently on all browsers, if your PWA is installed, when you browse to a URL that is out of the scope of your manifest, the browser engine of your PWA will render an in-app browser within the context of your window.
Some features of the in-app browsers are:
- They appear on top of your content.
- They have a static URL bar showing the current origin, the window's title, and a menu. Typically, they're themed with the
theme_colorof your manifest. - From the contextual menu, you can open that URL in the browser.
- Users can close the browser or go back.
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/0SsThiNz4GDkJy6OiTHj.png", alt="An in-app browser on a desktop PWA when browsing a URL that is out-of-scope.", width="800", height="497" %}
{% Aside %} While the in-app browser is on the screen, your PWA is waiting in the background, as if another window is obscuring it. {% endAside %}
{% Aside 'gotchas' %} On iOS and iPadOS, the in-app browser uses SafariViewController, a Safari-rendering engine isolated from your PWA storage. Therefore, if the user has a session in Safari, it won't appear in your PWA, but it will appear within an in-app browser in your PWA. {% endAside %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/Gr4qJUx6hAFMyPLPyfQY.png", alt="An in-app browser on an iPhone when browsing a URL that is out-of-scope within a standalone PWA.", width="800", height="370" %}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/UyohW4R5Sy79ffHri8cz.png", alt="An in-app browser on Android when browsing a URL that is out-of-scope within a standalone PWA.", width="800", height="1644" %}
Authorization flows
Many web authentication and authorization flows involve redirecting the user to a different URL in a different origin to acquire a token that will return to your PWA's origin, such as using OAuth 2.0.
In these cases, the in-app browser follows the following process:
- The user opens your PWA and clicks login.
- Your PWA redirects the user to a URL that is out of the scope of the PWA so that the rendering engine will open an in-app browser within your PWA.
- The user can cancel the in-app browser and go back to your PWA at any time.
- The user logs into the in-app browser. The authentication server redirects the user to your PWA origin, sending the token as an argument.
- The in-app browser closes itself when it detects a URL that is part of the scope of the PWA.
- The engine redirects the main PWA window navigation to the URL that the authentication server went to while in the in-app browser.
- Your PWA gets the token, stores the token, and renders the PWA.
Forcing a browser's navigation
If you want to force open the browser with a URL and not an in-app browser, you can use the _blank target of <a href> elements. This works only on desktop PWAs; on mobile devices, there is no option to open a browser with a URL.
function openBrowser(url) {
window.open("url", "_blank", "");
}
Opening new windows
On desktop, users can open more than one window of the same PWA. Each window will be a different navigation to the same start_url, as if you were opening two browser tabs of the same URL.
From the menu in the PWA, users can select File then New window, and from your PWA code, you can open a new window with the open() function. Check the documentation for details.
function openNewWindow() {
window.open("/", "new-window", "width=600,height=600");
}
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/CURym4RD66CqcTw4iPl5.png", alt="The same installed PWA with several windows opened on a desktop operating system.", width="800", height="580" %}
Calling open() within a PWA window on iOS or iPadOS returns null and doesn't open a window. Opening new windows on Android creates a new in-app browser for the URL—even if the URL is within the scope of your PWA—that typically doesn't trigger an external browsing experience.
{% Aside 'warning' %}
The second argument of open() is the window's name. If the name you used is already opened, the engine will replace the opened window with that name with a new navigation. If you always want to open a new window, you must use different string values for a name.
{% endAside %}
{% Glitch 'mlearn-pwa-windows-basic' %}
{% Aside 'gotchas' %}
The functions open(), moveTo(), and resizeTo() don't need the window. prefix when writing JavaScript because window is the global object. You can just call moveTo(0, 0). However, calling window.moveTo(0, 0) makes the code easier to understand.
{% endAside %}
Window title
The <title> element was primarily used for SEO purposes as the space within a browser tab is limited. When you move from the browser to your window in a PWA, all the title bar space is available to you.
You can define the contents of the title bar:
- Statically in your HTML
<title>element. - Dynamically changing the
document.titlestring property at any time.
On desktop PWAs, the title is essential, and it's used in the title bar of the window and sometimes in the task manager or multi-task selection. If you have a single-page application, you may want to update your title on every route.
{% Aside %} To reduce phishing within PWAs, some desktop browsers may have additional measures on the window's title. For example, if you don't use your app's name in the title, the browser may add a prefix with your PWA's name on it. Other browsers may render the current origin in the title bar for a few seconds when you change the title to highlight where the user navigated. {% endAside %}
Tabbed mode
An experimental capability, known as tabbed mode will let your PWA have a tab-based design similar to a web browser. In this case, the user can have several tabs opened from the same PWA but all tied together in the same operating system window, as you can see in the following video:
{% Video src="video/W3z1f5ZkBJSgL1V1IfloTIctbIF3/b1SL9QwJcgRXrVDeLKmK.mp4", loop="true", muted="true", playsinline="true", controls="true", poster="image/W3z1f5ZkBJSgL1V1IfloTIctbIF3/emg7quu4DyvYwlyzgDE1.png" %}
You can read more about this experimental capability at Tabbed application mode for PWA.
{% Aside %} In the Experimental chapter, you will see how you can start using experimental capabilities. {% endAside %}
Window controls overlay
We've mentioned that you can change the window's title by defining the value of the <title> element or the document.title property. But it's always a string value. What if we could design the title bar as we wish, with HTML, CSS, and images?
That's where Window Controls Overlay comes in, a new experimental capability in Microsoft Edge and Google Chrome for desktop PWAs.
{% Aside %}
Tabbed mode and window controls overlay capabilities define new values for the manifest's display member. To create compatibility with all devices, the manifest group has created a way to replace the fallback display chain, so you can specify what display value to use if your first option is not available. Read more about it at Preparing for the display modes of tomorrow.
{% endAside %}
You can read more about this capability at Customize the window controls overlay of your PWA's title bar.
{% Img src="image/kheDArv5csY6rvQUJDbWRscckLr1/B5UDAJzKTqCRdSy6B5is.png", alt="With window controls overlay, you can render content in the title bar.", width="800", height="544" %}
Multi-screen window placement
With multiple screens, users will want to use all the space available to them. Users may:
- Open a presentation on an external monitor.
- Restore an opened window's position on the screen.
- Favor screens that support touch.
The Multi-Screen Window Placement API allows PWAs to do just that and more.
Getting screen details
The Multi-Screen Window Placement API adds a new method, window.getScreenDetails(), that returns an object with screens as an immutable array of attached screens. There's also a live object accessible from ScreenDetails.currentScreen, corresponding to the current window.screen.
{% Aside 'gotchas' %}
On some browsers, calling window.getScreenDetails() requires the user to grant permission to your PWAs.
{% endAside %}
The returned object also fires a screenschange event when the screens array changes. (This does not happen when attributes on individual screens are changed.) Individual screens, either window.screen or a screen in the screens array, also fire a change event when their attributes change.
// Request an object with a screen objects
const screenDetails = await window.getScreenDetails();
screenDetails.screens[0].isPrimary; // e.g. true
screenDetails.screens[0].isInternal; // e.g. true
screenDetails.screens[0].pointerTypes; // e.g. ["touch"]
screenDetails.screens[0].label; // e.g. 'Samsung Electric Company 28"'
// Access the live object corresponding to the current `window.screen`.
// The object is updated on cross-screen window placements or device changes.
screenDetails.currentScreen;
screenDetails.addEventListener('screenschange', function() {
// NOTE: Does not fire on changes to attributes of individual screens.
const screenCount = screenDetails.screens.length;
const currentScreen screenDetails.currentScreen.id;
});
If the user or the operating system moves your PWA's window from one screen to another, a currentscreenchange event will also be fired from the screen details object.
{% Aside %} You may also want to check the Presentation API to display web content through large presentation devices such as projectors and network-connected televisions. {% BrowserCompat 'api.Presentation' %} {% endAside %}
{% Glitch 'mlearn-pwa-windows-screens' %}
Screen wake lock
Imagine this: you're in the kitchen following a recipe on your tablet. You've just finished prepping your ingredients. Your hands are a mess, and you turn back to your device to read the next step. Disaster! The screen's gone black! The Screen Wake Lock API is here for you and lets a PWA prevent screens from dimming, sleeping, or locking, allowing users to stop, start, leave, and return without worry.
// Request a screen wake lock
const wakeLock = await navigator.wakeLock.request();
// Listen for wake lock release
wakeLock.addEventListener('release', () => {
console.log(`Screen Wake Lock released: ${wakeLock.released}`);
});
// Manually release the wake lock
wakeLock.release();
Virtual keyboard
Touch-based devices, such as phones and tablets, offer a virtual on-screen keyboard so the user can type when form elements of your PWA are in focus.
Thanks to the VirtualKeyboard API, your PWA can now have more control of the keyboard on compatible platforms using the navigator.virtualKeyboard interface, including:
- Showing and hiding the virtual keyboard with the functions
navigator.virtualKeyboard.show()andnavigator.virtualKeyboard.hide(). - Telling the browser that you are taking care of closing the virtual keyboard yourself by setting
navigator.virtualKeyboard.overlaysContentequal totrue. - Knowing when the keyboard appears and disappears with the event
geometrychangeofnavigator.virtualKeyboard. - Setting the virtual keyboard policy on editing host elements (using
contenteditable) with thevirtualkeyboardpolicyHTML attribute. A policy lets you decide if you want the virtual keyboard to be handled automatically by the browser using theautovalue, or handled by your script using themanualvalue. - Using CSS environmental variables for getting information about the virtual keyboard appearance, such as
keyboard-inset-heightandkeyboard-inset-top.
You can read more about this API on Full control with the VirtualKeyboard API.
Resources
- Managing several displays with the Multi-Screen Window Placement API
- Tabbed application mode for PWAs
- Stay awake with the Screen Wake Lock API
- Full control with the VirtualKeyboard API
- Customize the window controls overlay of your PWA's title bar
- Display content in the title bar
Workbox
Maintaining your service worker and cache storage logic can be a challenge as your PWA grows. Workbox, is a set of open-source libraries to help with that. Workbox encapsulates the low-level APIs, like the Service Worker API and Cache Storage API, and exposes more developer-friendly interfaces.
Some tasks that it can help with are matching caching strategies to paths (or routing patterns), working with streams, and using features like background sync with proper fallbacks.
Workbox can help you with managing your asset caching and serving needs. It's also the most used library for service workers; used by 54% of mobile sites and it is used in many build tools and CLIs, including the Angular CLI, Create-React-App, and Vue CLI. There are plugins to most other libraries and frameworks, too, such as Next.js.
54%
Mobile sites with service workers use the Workbox library
{% Aside %} While Workbox is the most common set of libraries for service workers in the PWA community, there are other solutions such as sw-tools and UpUp {% endAside %}
Workbox modules
Workbox includes several libraries—called modules internally—each focused on a different aspect of managing your assets and service worker behavior.
Workbox modules work in different contexts, such as:
- Within a service worker context: you import the modules you need and use them from your service worker file, for example to help manage caching and serve files with different strategies.
- Within the main
windowcontext: helping to register a service worker and communicating with it - As part of a build system: for example, webpack, for purposes such as creating a manifest of your assets, or even generating your entire service worker.
Some popular modules are:
- workbox-routing: When the service worker intercepts requests this module routes those requests to different functions that provide responses; it's an implementation of the
fetchevent handler as mentioned in the Serving chapter. - workbox-strategies: A set of runtime caching strategies that handle responding to a request, such as cache first and stale while revalidate; it's an implementation of the different strategies mentioned in the Serving chapter.
- workbox-precaching: It's an implementation of caching files in the
installevent handler of the service worker (also known as precaching), as mentioned in the Caching chapter. With this module you can easily precache a set of files and efficiently manage updates to those assets. We'll cover updating assets in the Update chapter. - workbox-expiration: It is a plugin used with the caching strategies to remove cached requests based on the number of items in a cache or based on the age of the cached request. It helps manage your caches and sets limits on time and the number of items in each cache.
- workbox-window: A set of modules intended to run in the window context, which is to say, inside of your PWA web pages. You can simplify the process of service worker registration and updates and enable easier communication between code running in the service worker context and the window context.
Using Workbox
Workbox provides different ways to integrate into your PWA. You can choose which fits best with your app's architecture:
- Workbox CLI: A command-line utility that generates a complete service worker, injects a precache manifest, or copies needed Workbox files.
- Workbox Build: An npm module that generates a complete service worker, injects a precache manifest, and copies the Workbox files. This is meant to be integrated with your own build process.
- workbox-sw: A way to load Workbox service worker packages from a CDN that doesn't use a build process.
Workbox CLI provides a wizard that steps you through creating your service worker. To run the wizard, type the following at a command line:
npx workbox-cli wizard
{% Img src="image/RK2djpBgopg9kzCyJbUSjhEGmnw1/GTRYFMjNWIGjgp3Sqzba.png", alt="Workbox CLI in action in a terminal", width="800", height="394" %}
Caching and serving with Workbox
A common use of Workbox is using the routing and strategies modules together to cache and serve files.
The module workbox-strategies provides, out of the box, the caching strategies discussed in the Assets and data and Serving chapters.
The workbox-routing module helps to sort incoming requests to your service worker and match them to the caching strategies or functions to get responses for those requests.
After the matching of routes to strategies, Workbox also offers the ability to filter which responses will be added to the cache with the workbox-cacheable-response plugin. With this plugin you can for example cache only responses that returned without errors.
{% Aside 'caution' %} While Workbox is distributed as npm modules, service workers themselves don't support ES modules syntax in every browser, so it's better to compile this sample code with a bundler before deploying it. {% endAside %}
The following code sample uses a cache first strategy (via the CacheFirst module) to cache and serve page navigations.
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
const pageStrategy = new CacheFirst({
// Put all cached files in a cache named 'pages'
cacheName: 'pages',
plugins: [
// Only requests that return with a 200 status are cached
new CacheableResponsePlugin({
statuses: [200],
}),
],
});
The plugin allows you to tap into Workbox's caching and request resolution lifecycle. Here, the CacheableResponsePlugin is used to only cache requests that result in a 200 status, preventing bad requests from being saved into the cache.
With the strategy created, it's time to register a route to use it. The following example calls registerRoute(), passing a Request object to its callback. If request.mode is "navigate" it uses the CacheFirst strategy (here called pageStrategy) defined in the previous code example.
// Cache page navigations (HTML) with a Cache First strategy
registerRoute( ({ request }) => request.mode === 'navigate',
pageStrategy );
Read the Workbox documentation for more examples and best practices.
Offline fallback
The workbox-routing module also has an exported setCatchHandler(), that provides handling if a route throws an error. You can use this to set up an offline fallback to notify users that their requested route isn't currently available.
Here, a combination of Workbox and the Cache Storage API provides an offline fallback using a cache-only strategy.
First, during the service worker's install lifecycle, the offline-fallbacks cache is opened, and the array of offline fallbacks is added to the cache.
import { setCatchHandler } from 'workbox-routing';
// Warm the cache when the service worker installs
self.addEventListener('install', event => {
const files = ['/offline.html']; // you can add more resources here
event.waitUntil(
self.caches.open('offline-fallbacks')
.then(cache => cache.addAll(files))
);
});
Then, in setCatchHandler(), the destination of the request that threw an error is determined, and the offline-fallbacks cache is opened. If the destination is a document, the content of the offline fallback is returned to the user. If that doesn't exist, or the destination isn't a document (such as an image or stylesheet), an error response is returned. You can extend this pattern not just for documents but for images, videos, fonts, really anything that you'd want to provide as an offline fallback.
// Respond with the fallback if a route throws an error
setCatchHandler(async (options) => {
const destination = options.request.destination;
const cache = await self.caches.open('offline-fallbacks');
if (destination === 'document') {
return (await cache.match('/offline.html')) || Response.error();
}
return Response.error();
});
Recipes
Several routing and caching patterns, like NetworkFirst navigations and offline fallbacks, are common enough to be encapsulated into reusable recipes. Check workbox-recipes as they can help you if they provide a solution suitable for your architecture. They are usually available as one line of code that you need to add to your service worker's code.
Caching and updating assets
Caching assets also involves updating them. Workbox helps with updating your assets in whatever way you decide is best. It could be keeping them updated if they change on the server, or waiting until you have a new version of your app. You'll learn more about updating in the Update chapter.
Play with Workbox
You can play with Workbox right away using the following code lab:
{% Aside 'codelab' %} Working With Workbox. {% endAside %}