Build user-adaptive interfaces with preference media queries

211ff61d01be58e.png

User's have indicated many preferences on their devices these days. They want the operating system and apps to look and feel like theirs. User-adaptive interfaces are those which are ready to use these preferences to enhance the user experience, to make it feel more at home, to make it feel like theirs. If done correctly, a user may never know the user experience is adapting or has adapted.

User preferences

Device hardware choice is a preference, operating system is a choice, app and operating systems colors are preferences, and app and operating systems document languages are preferences. The amount of preferences a user has only seems to grow. A web page is not able to access everything and for good reason.

Here's a few examples of user preferences that can be used by CSS:

Here's a few examples of user preferences coming soon to CSS:

Media queries

CSS and the web enable adaptation and responsiveness through media queries, a declarative condition that contains a set of styles if that condition is true. The most common being a condition on viewport size of the device: if the size is less than 800 pixels, then here's some better styles for that case.

User-adaptive

An unadaptive interface is one that changes nothing when a user visits it, essentially delivering one experience to everyone with no ability to adjust. A user-adaptive interface could have five different appearances and styles for five different users. The functionality is the same, but the aesthetics are perceived better and usability of the interface is easier for users who can adjust the UI.

Prerequisites

What you'll build

In this codelab, you're going to build a user-adaptive form that adapts to the following:

  • The system color scheme preference by offering a light and dark color scheme for the form controls and surrounding UI elements
  • The system motion preferences by offering multiple types of animations
  • Small and large device viewports to offer mobile and desktop experiences
  • Various input types like keyboard, screen reader, touch, and mouse
  • Any language and reading/writing mode

de5d580a5b8d3c3d.png

What you'll learn

In this codelab, you learn about modern web features to help you build a user-adaptive form. You learn how to:

  • Make light and dark themes
  • Build animated and accessible forms
  • Layout responsive forms
  • Use relative units and logical properties

f142984770700a43.png

This codelab is focused on user adaptive interfaces. Non-relevant concepts and code blocks are glossed over and are provided for you to simply copy and paste.

What you'll need

  • Google Chrome 89 and higher, or your preferred browser

19e9b707348ace4c.png

Get the code

Everything you need for this project is in a GitHub repository. To get started, you'll need to grab the code and open it in your favorite developer environment. Alternatively, you can fork this Codepen and work from there.

Recommended: Use Codepen

  1. Open a new browser tab.
  2. Go to https://codepen.io/argyleink/pen/abBMeeq.
  3. If you don't have an account, create one to save the work.
  4. Click Fork.

Alternative: Work locally

If you want to download the code and work locally, you'll need to have Node.js version 12 or higher, and a code editor set up and ready to go.

Use Git

  1. Visit https://github.com/argyleink/Google-IO-2021-Workshop_User-Adaptive-Interfaces
  2. Clone the repository to a folder.
  3. Notice the default branch is beginning.

Use files

Download source code

  1. Unpack the downloaded zip file to a folder.

Run the project

Use the project directory established in either of the above steps, then:

  1. Run npm install to install the dependencies required to run the server.
  2. Run npm start to start the server on port 3000.
  3. Open a new browser tab.
  4. Go to http://localhost:3000.

About the HTML

This lesson will cover the aspects of the HTML used to support user-adaptive interactivity. This workshop has a specific focus on CSS. The HTML provided is worth reviewing if you're new to building forms or websites. HTML element choices can be critical when it comes to accessibility and layout.

When you're ready to start, this is the skeleton of what you'll be transforming into a dynamic and adaptive user experience.

de5d580a5b8d3c3d.png

Git branch: beginning

By the end of this section, your settings form will adapt to:

  • Gamepad + keyboard
  • Mouse + touch
  • Screen reader or similar assistive technology

Attributes for the HTML

The HTML provided in the source code is a great starting point because semantic elements to help group, order, and label your form inputs have already been chosen.

Forms are often a key interaction point for a business, so it's important the form can adapt to the many types of input the web can facilitate. For example, it's likely important to have a form that's usable on mobile with touch. In this section, before layout and style, you ensure adaptive-input usability.

Grouping inputs

The <fieldset> element in the HTML is for grouping similar inputs and controls together. In your form you have two groups, one for volume and one for notifications. This is important to the user experience so whole sections can be skipped.

Ordering elements

The order of the elements is provided in a logical order. This is important to the user experience so the visual experience order is the same or similar for gamepad, keyboard, or screen reader technologies.

Keyboard interaction

Users of the web have become accustomed to moving through forms with their tab key, which fortunately the browser takes care of if you provide the expected HTML elements. Using elements like <button>, <input>, <h2>, and <label> automatically become keyboard or screen-reader destinations.

9fc2218473eee194.gif

The above video demonstrates how the tab key and arrows can move through the interface and make changes. The blue outline though is very tight around the input elements, add the following styles to make this interaction have a little breathing room.

style.css

input {
  outline-offset: 5px;
}

Things to try

  1. Review the HTML elements used in index.html.
  2. Click the demo page in your browser.
  3. Press the tab key and shift+tab keys to move element focus through the form.
  4. Use the keyboard to change values of the sliders and checkboxes.
  5. Connect a bluetooth gamepad controller and move element focus through the form.

Mouse interaction

Users of the web have become accustomed to interacting with forms with their mouse. Try using your mouse on the form. Sliders and checkboxes work, but you can do better. Those checkboxes are quite small for a mouse to click.

ab51d0c0ee0d6898.gif

See how you get two user-experience features for connecting your labels and their inputs?

The first feature is having options for which to interact with and the label is much easier to target for a mouse than a tiny square.

The second feature is knowing exactly which input a label is for. Without CSS right now, it's quite difficult to determine which label is for which checkbox, unless you provide some attributes.

This explicit connection also improves the experience for screen readers, which are covered in the next section.

Unassociated: no attributes connecting the elements

<input type="checkbox">
<label>...</label>

Associated: attributes connecting the elements

<input type="checkbox" id="voice-notifications" name="voice-notifications">
<label for="voice-notifications">...</label>

The provided HTML has already attributed all inputs and labels. It's worth further investigation if this is a new concept to you.

Things to try

  1. Hover over a label with your mouse and notice the checkbox highlights.
  2. Investigate a label element with Chrome Developer Tools to visualize the clickable surface area that can select the checkbox.

Screen-reader interaction

Assistive technology can interact with this form and, with a few HTML attributes, can make the user experience smoother.

28c4a14b892c62d0.gif

For users navigating the current form with a screen reader in Chrome, there's an unnecessary stop at the <picture> element (not Chrome specific). A user with a screen reader is likely using the screen reader because of a vision disability, so stopping on a picture is not helpful. You can hide elements from screen readers with an attribute.

index.html

<picture aria-hidden="true">

Now screen readers pass over the element that was purely visual.

f269a73db943e48e.gif

The slider element input[type="range"] has a special ARIA attribute on it: aria-labelledby="media-volume". This provides special instruction for the screen reader to use to enhance the user experience.

Things to try

  1. Use your operating-system screen-reader technology to move focus through the form.
  2. Download and try some screen-reader software on the form.

Git branch: layouts

By the end of this section, the settings page will:

  • Create a spacing system with custom properties and user relative units
  • Write CSS Grid for flexible, responsive alignment and spacing
  • Use logical properties for internationally adaptive layouts
  • Write media queries to adapt between compact and spacious layouts

f142984770700a43.png

Spacing

A key to a nice layout is a limited palette of spacing options. This helps content find natural alignments and harmonies.

Custom properties

This workshop builds upon a set of seven custom property sizes.

  • Put these at the top of style.css:

style.css

:root {
  --space-xxs: .25rem;
  --space-xs:  .5rem;
  --space-sm:  1rem;
  --space-md:  1.5rem;
  --space-lg:  2rem;
  --space-xl:  3rem;
  --space-xxl: 6rem;
}

The naming is near the verbiage that folks would use amongst each other to describe space. You also use rem units exclusively for legible whole unit sizing that adapts and is mindful of a user's preference.

Page styles

Next, you need to set some document styles, remove margins from elements, and set the font to a nice sans-serif.

  • Add the following to style.css:

style.css

* {
  box-sizing: border-box;
  margin: 0;
}

html {
  block-size: 100%;
}

body {
  min-block-size: 100%;  
  padding-block-start: var(--space-xs);
  padding-block-end: var(--space-xs);
}

There's your first use of the spacing custom properties! This begins your space journey.

Typography

The font for this layout is adaptive. The system-ui keyword will use whatever the user's operating system has decided is the optimal interface font.

body {
  font-family: system-ui, sans-serif;
}

h1,h2,h3 { 
  font-weight: 500;
}

small {
  line-height: 1.5;
}

The styles for h1, h2, and h3 are minor and stylistic. The small element, though, needs the additional line-height for when text wraps. It's otherwise too bunched up.

Logical properties

Notice the padding on body is using logical properties (block-start, block-end) to specify the side. Logical properties will be used extensively throughout the rest of the codelab. They too, like a rem unit, adapt to a user. This layout can be translated to another language, and set to the natural writing and document directions the user is accustomed to in their native language. Logical properties unlock support for this with only one definition of space, direction, or alignment.

ce5190e22d97156e.png

Grid and flexbox are flow-relative already, meaning the styles written for one language will be contextual and appropriately applied for others. Adaptive directionality; content flows relative to the document directionality.

Logical properties let you reach more users with less styles to write.

CSS Grid Layouts

The grid CSS property is a powerful layout tool with many features for tackling complex tasks. You'll be building a few simple grid layouts and one complex layout. You'll also be working from the outside in, from macro layouts to micro layouts. Your spacing custom properties will become critical as not just padding or margin values, but column sizes, border radii, and more.

Here's a screenshot of Chrome DevTools overlaying each CSS grid layout at one time:

84e57c54d0633793.png

  1. Follow along by adding each of the following styles to style.css:

<main>

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
  padding: var(--space-sm);
}

Grid by default puts each child element into its own row, which makes it great for stacking elements. It also has the added benefit of using gap. Earlier, you set margin: 0 on all elements with the * selector, which is now important as you use gap for your spacing. Gap is not only a single place to manage space in a container, but its flow relative.

<form>

form {
  max-width: 89vw;
  display: grid;
  gap: var(--space-xl) var(--space-xxl);
  align-items: flex-start;
  grid-template-columns: 
    repeat(auto-fit, minmax(min(10ch, 100%), 35ch));
}

This is the trickiest grid layout of the design, but accounts for the most exciting responsive aspect:

  • max-width is providing a value for the layout algorithm to use when deciding how large it can be.
  • gap is using custom properties and passing a different row-gap from column-gap.
  • align-items is set to flex-start as to not stretch item heights.
  • grid-template-columns has some complex syntax, but the goal is succinct; keep columns 35ch wide and never less than 10ch, and put things into columns if there's room, otherwise rows are good.
  1. Test out resizing the browser. Watch as the form collapses to rows in a small viewport, but flow in new columns if there's room, adapting without media queries. This strategy of media-query free responsive styles is particularly useful for components or content-centric layouts.

<section>

section {
  display: grid;
  gap: var(--space-md);
}

Each section should be a grid of rows with medium space between child elements.

header {
  display: grid;
  gap: var(--space-xxs);
}

Each header should be a grid of rows with extra extra small space between child elements.

<fieldset>

fieldset {
  padding: 0;
  display: grid;
  gap: 1px;
  border-radius: var(--space-sm);
  overflow: hidden;
}

This layout is responsible for creating a card-like appearance and grouping inputs together. overflow: hidden and gap: 1px become clear when you add color in the next section.

.fieldset-item

.fieldset-item {
  display: grid;
  grid-template-columns: var(--space-lg) 1fr;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
}

This layout is responsible for centering the icon and checkbox with its associated labels and controls. The first column of the grid template, var(--space-lg), creates a column wider than the icon, so a child element has somewhere to be centered within.

This layout demonstrates how many design decisions have already been made in the custom properties. Padding, gaps, and a column were all sized within the harmony of the system by using values you've already defined.

.fieldset-item <picture>

.fieldset-item > picture {
  block-size: var(--space-xl);
  inline-size: var(--space-xl);
  clip-path: circle(50%);
  display: inline-grid;
  place-content: center;
}

This layout is responsible for settings, the size of the icon circle, creating a circle shape, and centering an image within it.

<picture> & [checkbox] alignment

.fieldset-item > :is(picture, input[type="checkbox"]) {
  place-self: center;
}

This layout isolates centering to picture and checkbox elements using the :is pseudo selector.

  1. Replace the selector picture > svg with .fieldset-item svg like this:

.fieldset-item <svg>

.fieldset-item svg {
  block-size: var(--space-md);
}

This sets the svg icon size to a value from the size system.

.sm-stack

.sm-stack {
  display: grid;
  gap: var(--space-xs);
}

This utility class is for the checkbox label elements to space the helper text for the checkbox.

input[type="checkbox"]

input[type="checkbox"] {
  inline-size: var(--space-sm);
  block-size: var(--space-sm);
}

These styles increase the size of a checkbox using values from our spacing set.

Things to try

  1. Open Chrome Developer Tools and find Grid badges on the HTML in the Elements panel. Click them to turn on the debug tools.
  2. Open Chrome Developer Tools and hover a gap in the Styles pane.
  3. Open Chrome Developer Tools, go to the Styles pane, and switch from Styles to Layouts. Explore this area by toggling it's settings and turning on layouts.

Media queries

The following CSS adapts the styles based on viewport size and orientation with the intent to adjust spacing or arrangement to be optimal given the viewport context.

<main>

@media (min-width: 540px) {
  main {
    padding: var(--space-lg);
  }
}

@media (min-width: 800px) {
  main {
    padding: var(--space-xl);
  }
}

These two media queries give main more padding as more viewport space is available. This means it starts in a compact, small amount of padding, but now becomes more and more spacious as more space becomes available.

<form>

form {
  --repeat: auto-fit;
  grid-template-columns: 
    repeat(var(--repeat), minmax(min(10ch, 100%), 35ch));
}

@media (orientation: landscape) and (min-width: 640px) {
  form {
    --repeat: 2;
  }
}

The form was responsive to viewport size already with auto-fit, but while testing on a mobile device, turning a device to landscape doesn't put the two form groups side by side. Adapt to this landscape context with an orientation media query and a viewport width check. Now, if the device is landscape and at least 640 pixels wide, force two columns by switching the --repeat custom property to a number instead of the auto-fit keyword.

.fieldset-item

@media (min-width: 540px) {
  .fieldset-item {
    grid-template-columns: var(--space-xxl) 1fr;
    gap: var(--space-xs);
    padding: var(--space-md) var(--space-xl) var(--space-md) 0;
  }
}

This media query is another spacing expansion when more viewport space is available. The grid template expands the first column by using a larger custom property (var(--space-xxl)) in the template. The padding is pumped up to larger custom properties as well.

Things to try

  1. Expand and contract your browser, and watch as space adjusts.
  2. Preview on a mobile device
  3. Preview on a mobile device in landscape

Git branch: colors

By the end of this section, your settings form will:

  • Adapt to dark and light color preferences
  • Have a color scheme derived from a brand hex
  • Have accessible colors

19e9b707348ace4c.png

HSL

In the next section, you'll be create a color system with HSL to help you make a light and dark theme. It's built upon this core concept in CSS: calc().

HSL stands for hue, saturation, and lightness. Hue is an angle, like a point on a clock, while saturation and lightness are percentages. calc() is able to do math on percentages and angles. You can perform lightness and saturation calculations on those percentages in CSS. Combine color channel calculations with custom properties, and you're in for a modern, dynamic color scheme where variants are computed off a base color, helping you avoid managing handfuls of colors in code.

5300e908c0c33d7.png

Custom properties

In this section, you build a set of custom properties for use within the rest of your styles. Similar to the spacing set you made earlier in the :root tag, you'll be adding colors.

Assume the brand color for your app is #0af. Your first task is to convert this hex color value into an HSL color value: hsl(200 100% 50%). This conversion reveals your brand's color channels in HSL, which you can use calc() on to compute various supporting brand colors.

Each of the following code blocks in this section should be added into the same :root selector.

Brand channels

:root {
  --hue: 200;
  --saturation: 100%;
  --lightness: 50%;
}

The three HSL channels have been extracted and placed into their own custom properties.

  1. Use all three properties as is and recreate the brand color.

Brand

:root {
  --brand: hsl(
    var(--hue) 
    calc(var(--saturation) / 2)
    var(--lightness) 
  );
}

Since your color scheme is dark by default, it's good practice to desaturate colors for use on dark surfaces (they can otherwise vibrate to the eye or be inaccessible). To desaturate your brand color, you use the hue and lightness as is, but cut the saturation in half with some division: calc(var(--saturation) / 2). Now your brand color is properly on theme, but desaturated for use.

Text

:root {
  --text1: hsl(var(--hue) 15% 85%);
  --text2: hsl(var(--hue) 15% 65%);
}

For the reading text in our dark theme, you use the brand hue as a base, but build nearly white colors from it. Many users will think the text is white, while it's actually very light blue. Staying within hue is a strong way to create design harmony. --text1 is 85% white and --text2 is 65% white, and both have very little saturation.

  1. After adding the code to your project, open Chrome Developer Tools and explore changing these channel values. Feel how HSL and it's channels interact with each other. Maybe your taste wants more or less saturation.

Surface

:root {
  --surface1: hsl(var(--hue) 10% 10%);
  --surface2: hsl(var(--hue) 10% 15%);
  --surface3: hsl(var(--hue) 5% 20%);
  --surface4: hsl(var(--hue) 5% 25%);
}

The text is very light because surfaces will be dark in dark mode. Where text colors were using high lightness values (85% and higher), surfaces will use lower values (30% and lower). Having a healthy span between lightness ranges between surface and text will help ensure accessible colors for users to read.

  1. Notice how the colors start at the darkest grey with 10% lightness and 10% saturation, then desaturate as they become lighter. Each new surface is 5% lighter than the previous. Saturation is dropped a little too in the lighter surfaces. Try putting your surfaces all to 10% saturation. Do you like it more or less?

Light theme

With a healthy set of text and surface colors specifying the dark theme, it's time to adapt to a light theme preference by updating the color custom properties inside a prefers-color-scheme media query.

You'll use the same technique of keeping a large difference in lightness values between your surfaces and text colors to keep colors contrasting well.

Brand

@media (prefers-color-scheme: light) {
  :root {
    --brand: hsl(
      var(--hue) 
      var(--saturation)
      var(--lightness) 
    );
  }
}

First up is the brand color. It needs the saturation restored to its full power.

Text

@media (prefers-color-scheme: light) {
  :root {
    --text1: hsl(
      var(--hue) 
      var(--saturation)
      10% 
    );

    --text2: hsl(
      var(--hue) 
      calc(var(--saturation) / 2)
      30%
    );
  }
}

Similar to how the dark theme had very light blue text colors, in the light theme the text colors are very dark blue. Seeing 10% and 30% as the lightness values for the HSL color should signal to you that these colors are dark.

Surface

@media (prefers-color-scheme: light) {
  :root {
    --surface1: hsl(var(--hue) 20% 90%);
    --surface2: hsl(var(--hue) 10% 99%);
    --surface3: hsl(var(--hue) 10% 96%);
    --surface4: hsl(var(--hue) 10% 85%);
  }
}

These surface colors are the first to break patterns. What could have appeared as pretty reasonable and linear so far is now broken. The nice thing is that you can play with HSL light-theme color combinations right here in code, and adjust lightness and saturation to create a nice light color scheme that's not too cool or blue.

Use the color system

Now that the colors are defined, it's time to use them. You have a nice popping accent brand color, two text colors, and four surface colors.

  • For the following code sections, find the matching selector and add the color CSS to the existing code block.

<body>

body {
  background: var(--surface1);
  color: var(--text1);
}

The page primary colors are the first surface and text colors you made, which also puts the default amount of contrast at its maximum. Light and dark toggling can begin to be tested!

<fieldset>

fieldset {
  border: 1px solid var(--surface4);
  background: var(--surface4);
}

This is the card-like element of your design. The 1 pixel border and the 1 pixel gap are the same color and represent the surface behind each .fieldset-item. This creates nice visual harmony and is easy to maintain.

.fieldset-item

.fieldset-item {
  background: var(--surface3);
}

Each form input is on it's own surface. Hopefully, you're seeing how these are coming together and how the variations of lightness are layering together.

.fieldset-item > picture

.fieldset-item > picture {
  background: var(--surface4);
}

This is a stylistic choice to showcase the circle shape surrounding the icon. It becomes apparent why when you add interactions in the next section.

.fieldset-item svg

.fieldset-item svg {
  fill: var(--text2);
}

The icons in the form are set to use the alt text --text2. Designs where filled icons are slightly lighter than text keeps them from feeling too heavy.

.fieldset-item:focus-within svg

.fieldset-item:focus-within svg {
  fill: var(--brand);
}

This selector matches the input container element when one of the inputs inside is being interacted with and targets the SVG to highlight it with your brand accent. This provides the nice UX feedback of the form, where interacting with inputs highlights their relevant iconography.

<small>

small {
  color: var(--text2);
}

It's small text. It should be slightly subdued when compared to headers and paragraphs (primary content).

Dark form controls

:root {
  color-scheme: dark light;
}

This nice final touch tells the browser that this page supports both dark and light themes. The browser rewards us with dark form controls.

Git branch: animations

By the end of this section, the settings page will:

  • Adapt to the user's motion preferences
  • Respond to user interaction

b23792cdf4cc20d2.gif

Reduced motion versus no motion

The user preference found in the operating system for motion does not offer a value of no animation. The option is to reduce motion. Crossfade animations, color transitions, and more are still desirable for users who prefer reduced motion.

In this settings page, there isn't a lot of motion in terms of movement across the screen. The motion is more of a scale effect, as if the element is traveling toward the user. It's so trivial to adjust your CSS code to accommodate reduced motion that you reduce the scaling transitions.

Interaction styles

<fieldset>

fieldset {
  transition: box-shadow .3s ease;
}

fieldset:focus-within {
  box-shadow: 0 5px 20px -10px hsl(0 0% 0% / 50%);
}

When a user is interacting with the inputs of one of the <fieldset> card-looking elements, this adds a lifting effect. The interface is pushing an element forward, helping the user focus as the contextual form group is brought toward the user.

.fieldset-item

.fieldset-item {
  transition: background .2s ease;
}

.fieldset-item:focus-within {
  background: var(--surface2);
}

When the user is interacting with an input, the specific item layer background changes to a highlighted surface color, another supportive interface feature to help draw the user's attention and signal the input is being received. Color transitions do not need to be reduced in most cases.

.fieldset-item > picture

@media (prefers-reduced-motion: no-preference) {
  .fieldset-item > picture {
    clip-path: circle(40%);
    transition: clip-path .3s ease;
  }

  .fieldset-item:focus-within picture {
    clip-path: circle(50%);
  }
}

Here is a clip-path animation you only use if the user has no preferences when it comes to reduced motion. The first selector and styles constrict the circle clip path by 10% and set some transition parameters. The second selector and style waits for users to be interacting with an input, then scale up the icon's circle. A subtle, but neat effect when it is ok.

Git branch: complete

Congratulations, you've successfully built a user-adaptive interface!

You now know the key steps required to making interfaces that adapt to various user scenarios and settings.