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; } }
- The following snippet toggles between two buttons, hiding the other depending on the active device
-
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
- A better solution is to use the most common device resolution and place it in the middle of each grouping
-
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); }