Graduate Program KB

Module 5 - Response and Behavioural CSS

  • Factors to consider when building user interfaces
    • Screen size/orientation
    • Device type (phone, tablet, laptop, desktop, watch, etc.)
    • Input mechanism (mouse, trackpad, touch, keyboard, dictation)
    • User preferences/settings (themes, contrast, reduced motion)
    • Zoom level, font size
    • Device and network performance

Working with Mobile Devices

  • Most phones come with a high DPI display, typically having a higher resolution than most monitors

    • The device pixel ratio is the ratio between physical LED pixels and "theoretical" pixels used in CSS

      • For an iPhone, this ratio is 3, so 10px in length is actually 30px long
      • Each software pixel corresponds to 9 hardware pixels
      img {
          /* CSS will display a 150x150 size image on iPhone */
          width: 50px;
          height: 50px;
      }
      
    • We can find the device pixel ratio of a device with JavaScript

    console.log(window.devicePixelRatio);
    
  • To ensure pages render correctly on mobile, include the <meta> tag to the HTML file

    • width=device-width instructs the browser to set the viewport width to match the device's width
    • initial-scale=1 sets the default zoom level to 1x
    <meta
        name="viewport"
        content="width=device-width, initial-scale=1"
    >
    

Mobile Testing

  • Regularly test web applications on mobile devices, using iPhone and Android

    • Use BrowserStack if you don't have physical access to these devices
    • Allows you to load a site on a device and interact with it
    • However, it's not free and there could be latency issues
  • Setting up the testing environment:

    • Accessing localhost on a phone:

      • Use a tunnelling service like ngrok
      • Run on development machine, it will generate a random URL that the mobile can access and forward you to the localhost server
      • There is a paid version, but the free version is sufficient
      ngrok http 3000
      
      Forwarding          http://ff2b68d21f8e.ngrok.io -> http://localhost:3000
      
    • Opening devtools for mobile browser on our desktop for remote debugging

      • For iOS, follow WebKit instructions to enable remote debugging

        • Plug phone into the desktop and allow access
        • Visit the website you want to debug
        • For macOS debugging, open Safari and select Develop > phone name > tab name
        • For Windows debugging, one of the best tools is Inspect, it allows you to debug iOS devices using Chrome developer tools
      • For Android, follow these instructions regardless of operating system

        • Open DevTools window connected directly to your Android Chrome session
      • There are also options for debugging directly on mobile, such as Eruda and InspectBrowser

Media Queries

  • Using the @media keyword, we can selectively apply rules based off one or more conditions

    • Similar to conditional statements in JavaScript
    • Rules are merged together
    • Media queries don't affect specificity, if the order of the @media and the first .signup block switched then the button never changes size
      • It would be like increasing the font size if width was less than or equal to 400px, but then just setting it back to default right after anyway
      • And if the width was greater than 400px, the font size would have been default regardless
      • Always apply the default setting first, then modify the value based off conditions after
    .signup {
        color: blue;
        font-size: 1rem;
    }
    
    @media (max-width: 400px) {
        .signup-button {
            font-size: 2rem;
        }
    }
    
  • We can nest media queries within component definitions using styled-components

    • Enhances development experience, typically for regular media queries, CSS declarations are sectioned into device categories
    /* Mobile styles */
    ...
    /* Tablet styles */
    ...
    /* Desktop styles */
    ...
    
    • With nested media queries, declarations of an element for every device are in a single spot
    const SignupButton = styled.button`
        color: blue;
        font-size: 1rem;
    
        @media (max-width: 400px) {
            font-size: 2rem;
        }
    `;
    
  • There are two distinct mental models of writing media queries

    • Both ways have the same result, they are just written differently
    /* Desktop-first targets desktop devices by default, specifying overrides after */
    .signup {
        color: blue;
        font-size: 1rem;
    }
    
    @media (max-width: 400px) {
        .signup-button {
            font-size: 2rem;
        }
    }
    
    /* Mobile-first** targets mobile devices by default, specifying overrides after */
    .signup {
        color: blue;
        font-size: 2rem;
    }
    
    @media (min-width: 401px) {
        .signup-button {
            font-size: 1rem;
        }
    }
    
  • Try not to complicate the mental model, pick a default device and consistently use a single set of media queries to keep the structure simple

    • The following snippet toggles between two buttons, hiding the other depending on the active device
      • There's two sets of media queries for each device, not ideal
      • A minor issue is a viewport width of 600.5px is possible according to specification, which slips through these conditions
    @media (max-width: 600px) {
        .desktop-button {
            display: none;
        }
    }
    
    @media (min-width: 601px) {
        .mobile-button {
            display: none;
        }
    }
    
    • The following snippet is a refactored version using a desktop-first mental model
      • No more mixing media queries
      • Potential issue with non-integer viewport dimensions is now gone
    .mobile-button {
        display: none;
    }
    
    @media (max-width: 600px) {
        .mobile-button {
            display: revert;
        }
    
        .desktop-button {
            display: none;
        }
    }
    
  • Generally using rem media queries is preferred in most situations, it's provides accessibility to scale based off font size

    • Users can set default font size in the browser settings
    • Potential for better/worse UI experience depending on view size of device, always test

Other Queries

  • Hovering is a gesture only possible with pointer devices

    • Typically, only a mouse or trackpad should be able to trigger this state
    • But mobile developers made it so tapping an interactive element (like buttons or links) triggered the hover state until you tap somewhere else
    • To disable hover styles on mobile devices:
    @media (hover: hover) and (pointer: fine) {
        .button:hover {
            text-decoration: underline;
        }
    }
    
  • Boolean logic can be used in media queries, just like in the previous example with the and keyword

  • Media queries can also "hook in" and access user preferences

    • We can create a tailored style for each user, optimising accessibility and user experience
    • For instance, detecting whether users prefer light or dark mode, checking if users are sensitive to motion, etc.
    @media (prefers-color-scheme: dark) {
        ...
    }
    
    @media (prefers-reduced-motion: no-preference) {
        ...
    }
    

Breakpoints

  • Breakpoints are specific viewport widths that allow us to segment all devices into a set of possible experiences

    • If we had a breakpoint at 500px, users with a 375px or 414px wide phones would share the same layout
  • Typically, breakpoint values are selected based off common device resolutions

    • A better solution is to use the most common device resolution and place it in the middle of each grouping
      • Don't bother trying to sub-categorise the same device types into small or large
      • A 375px wide phone should be using the same layout as a 320px and 414px wide phone for example
    • General groups for devices:
      • Mobile: 0 - 550px
      • Tablet: 550px - 1100px
      • Laptop: 1100px - 1500px
      • Desktop: 1500+px
  • Here's a layout example of implementing breakpoints using a mobile-first model:

    /* Default: Phones from 0px to 549px */
    
    @media (min-width: 550px) {
        /* Tablets */
    }
    
    @media (min-width: 1100px) {
        /* Laptops */
    }
    
    @media (min-width: 1500px) {
        /* Desktops */
    }
    
  • There could be times when it's useful to create an exclusive query for a single device size, you can use and to specify min-width and max-width

    • Breakpoints aren't perfect, it won't cover 100% of cases
    • However, if custom values are used too often then maybe the breakpoints are in the wrong spot
  • CSS has no in-built way of managing breakpoints, media queries just take raw values like the pixel size

    • Using styled-components, we can solve this issue by declaring queries and importing them to parts of the application that need it
    • You can also use rem-based breakpoints by just using ${BREAKPOINTS.tabletMin / 16}rem instead
    const BREAKPOINTS = {
        tabletMin: 550,
        laptopMin: 1100,
        desktopMin: 1500
    }
    
    const QUERIES = {
        'tabletAndUp': `(min-width: ${BREAKPOINTS.tabletMin}px)`,
        'laptopAndUp': `(min-width: ${BREAKPOINTS.laptopMin}px)`,
        'desktopAndUp': `(min-width: ${BREAKPOINTS.desktopMin}px)`
    }
    
    import { QUERIES } from '../../constants';
    
    const Wrapper = styled.div`
        padding: 16px;
    
        @media ${QUERIES.tabletAndUp} {
            padding: 32px;
        }
    `;
    

CSS Variables

  • CSS variables function exactly like properties

    • Variables aren't being set, properties are being created
    • They start with two dashes -- to identify custom properties
    • Custom properties are inheritable
    strong {
        display: block;
        color: red;
        --favorite-food: tomato;
        --temperature: 18deg;
    }
    
  • Only available to its element and children, CSS variables are not globally accessible

    • The misconception comes from CSS variables being hung on :root, an alias for the root <html> tag
    • CSS variables can be attached to any selector, not just the root of the HTML document
    • Here's an example of disabling inheritance:
    @property --text-color {
        syntax: '<color>';
        inherits: false;
        initial-value: black;
    }
    
  • var function takers two arguments, the second is a default value

    button {
        padding: var(--inner-spacing, 16px);
    }
    
  • CSS variables exist in the browser, which means their values can dynamically change

    • Previously, CSS preprocessors like Sass or Less resolved the variables during compile time
  • JavaScript and CSS variables can work together to form new custom properties

    <Wrapper style={{ '--main-color': status === 'success' ? 'green' : 'red' }}>
        {children}
    </Wrapper>
    
  • We can take advantage of CSS variables being reactive to develop a response UI

    • According to Apple's Human Interface Guidelines, they recommend a minimum tap size of 44px x 44px
    • A global variable can be defined to apply this minimum tap size to many styled components with a coarse pointer
    const GlobalStyles = createGlobalStyle`
        @media (pointer: coarse) {
            html {
                --min-tap-height: 44px;
            }
        }
    `;
    
    const TextInput = styled.input`
        min-height: var(--min-tap-height, 32px);
    `;
    

Variable Fragments

  • CSS variables are evaluated when they're used, not when they're defined

    body {
        --border-width: 4px;
    }
    
    strong {
        ---border-details: dashed blue;
        border: var(--border-width) var(--border-details);
    }
    
  • They're also composable, nesting variables to form other variables

    body {
        --blue-hue: 275deg;
        --intense: 100% 50%;
        --color-primary: hsl(var(--blue-hue) var(--intense));
    }
    

The Magic of Calc

  • CSS can do math, using the classic 4 mathematical operators

    width: calc(100px + 24px);
    width: calc(100px - 24px);
    width: calc(100% / 4);          /* 25% of available space */
    width: calc(24px * 2);
    
  • The rem unit is better for setting font sizes because users can adjust it

    • But harder to work with because they're generally multiples of 16 rather than 10
    • We can use calculations to convert pixels to rems
    h1 {
        font-size: calc(24 / 16 * 1rem);    /* 24px converted to 1.5rem */
    }
    
  • Can take composition to the next level by using calc to adjust colour palettes

    :root {
        --red-hue: 0deg;
        --intense: 100% 50%;
        --orange: hsl(
            calc(--red-hue) + 20deg
            var(--intense)
        );
    }
    

Viewport Units

  • There are two main viewport units, vw (viewport width) and vh (viewport height)

    • 1vw is equivalent to 1% of the viewport width
    • Can be used with any property that accepts a <length> unit
  • Useful for ensuring an element is exactly as tall as the viewport

    • Issues with mobile, it includes a different UI with the address bar on top and array of buttons on the bottom
    • Therefore, the vh unit is generally referred to as the largest possible height
    • There are new units we can use:
      • svh: Refers to the smaller viewport height, the height that shows when a page loads
      • lvh: Refers to the full viewport height, once the browser UI has shrunk down
      • dvh: Dynamically adjust as the viewport height changes, it can be used as a way to get the height: 100% alternative to work
  • vw also has issues with the viewport on desktop not factoring the scrollbar element

    • If 100vw is set and the scrollbar is 15px wide, there will be 15px of horizontal overflow
    • It's fine on mobile because the scrollbar doesn't take width, it floats transparently above the content
  • Other units include vmin and vmax

    • vmin refers to the shorter dimension, 50vmin equals 50vw on a portrait orientation
    • vmax refers to the longer dimension, 50vmin equals 50vh on a landscape orientation

Clamping Values

  • Consider a column of text occupying 65% of the viewport and centered with margining

    • Doesn't scale well with large screens, it becomes too wide
    • On small screens with phones, the column is too narrow
    • Setting min-width and min-height might not factor the screen dimensions of all devices, causing overflow
  • The clamp function takes in 3 arguments and is a value that can be assigned to a property

    • The minimum value, ideal value and maximum value
    • The two rules below are identical
    .column {
        min-width: 500px;
        width: 65%;
        max-width: 800px;
    }
    
    .column {
        width: clamp(500px, 65%, 800px);
    }
    
    • Now that max-width is freed up, we can use it to apply two maximum widths
    • The element will never be larger than 800px or 100% of the available space
    .column {
        width: clamp(500px, 65%, 800px);
        max-width: 100%;
    }
    
  • clamp specifies a lower and upper bound, but if you want to limit only one bound, use min and max

    .box {
        padding: min(32px, 5vw);
    }
    

Scrollburglars

  • Not the proper term, but scrollburglar refers to a webpage with an accident horizontal scrollbar that allows you to scroll by a few pixels

  • It can be triggered by lots of different things, making it hard to debug

    • An element has an explicit width too large to fit in the container
    • A replaced element (video, image, etc.) is used without constraining its width to fit in the container
    • A really long word forces an element to be too wide for its parent container
    • An element is explicitly pulled outside of the parent (negative margins, etc.)
  • The first step is finding the specific element causing the overflow and fixing it

    • Can look at the box of the elements and see what leaks out of the parent container
  • The second step is to find a way to prevent this issue from reoccurring

  • The following JavaScript snippet can be run in console to quickly identify elements wider than the viewport

    • It might not find all scrollburglars, but it's good enough
    function checkElementWidth(element) {
        if (element.clientWidth > window.innerWidth) {
            console.info(
                "The following element has a larger width than " +
                "the window’s outer width"
            );
            console.info(element);
            console.info("\n\n");
        }
    
        // Recursively check all the children of the element to find the culprit
        [...element.children].forEach(checkElementWidth);
    }
    
    checkElementWidth(document.body);
    

Responsive Typography

  • The size of text should dynamically adjust depending on the viewing device, set by breakpoints with media queries

  • "Body text" is the text in paragraphs and lists, it's the baseline text filling most of the pages

    • It's font size should stay the same, regardless of display size
    • Generally, the body text should be at least 16px, which is the same as saying it should be at least 1rem
  • Smaller text includes annotations, labels, photo captions, etc.

    • If the text is unimportant, it might be better to just let users zoom in to read it
    • Otherwise, the font size can be increased on mobile to improve readability
  • Form input fields like <input> and <select> have a small default font size, making them hard to read on mobile

    • iOS Safari automatically zooms in when fields are focused to compensate, but only if fields are smaller than 16px
    • It will zoom so the input text is equivalent to 16px, we can just omit the zooming feature but setting it ourselves
    input, select, textarea {
        font-size: 1rem;
    }
    

Fluid Typography

  • Fluid typography enables us to smoothly scale typography with the viewport rather than discretely setting font sizes at specific breakpoints

  • It can be done with the vw unit, but there are two issues

    • It gets too large on very large screens and too small on very small screens
    • We can resolve this with clamp, guaranteeing the text is within a boundary no matter the viewport size
    h1 {
        font-size: clamp(1.5rem, 6vw, 3rem);
    }
    
    • However, an accessibility violation is introduced. Setting viewport units for font sizes lock text at that size
    • It can't be increased by zooming in or increasing the browser's default font size, violating WCAG guidelines that text should be at least 200% scalable
    • This problem an be fixed by combining the vw unit with a relative unit such as rem
      • Don't need to explicitly use calc, all calc-style equations are resolved within functions like clamp, min and max
      • Each vw unit is 1% of the viewport width of some amount of px units
      • 1rem is equivalent to 16px, adding this onto the converted vw unit gives the calculated font size
      • The rem portion of the equation is controlled by the user's font size, if it's doubled, then 1rem becomes 32px, not 16px
      • Basically, mixing a relative unit gives the user control over the font size although at a slower rate
    h1 {
        font-size: clamp(1.5rem, 4vw + 1rem, 3rem);
    }
    

Fluid Calculator

  • Can manipulate the rate of change of font size by setting the ratio between viewport and relative units
    .normal {
        font-size: clamp(2rem, 5vw + 1rem, 5rem);
    }
    
    .fast {
        font-size: clamp(2rem, 14vw - 1.5rem, 5rem);
    }