Module 9 - Little Big Details
CSS Filters
- SVGs have a rich filter API, can be used to create complex effects
- Access the SVG filter using the filter property
- Supports hardware acceleration in most browsers
.box { filter: drop-shadow(4px 8px 5px hsl(0deg 0% 0% / 0.2)); }
Colour Manipulation
-
Lots of filters manipulate colour in some way, the following examples take in a percentage value
- Functions: brightness, contrast, sepia, contrast, grayscale
- Multiple filters can be chained together on the same element by space-separating them
-
The function hue-rotate shifts the colour of every pixel in the element
- Given an initial hue of 260deg from hsl(260deg 80% 40%)
- When applying hue-rotate(60deg) on hover, it increases the hue of all colours by 60deg to a total 320deg
- Looking at the colour wheel, the colour starts from blue to red when applying a 60 degree clockwise shift
Blur Filter
- The blur function applies a Gaussian blur to an element
- Expensive operation even with hardware acceleration
- By default it will create a soft, blurred edge. Add overflow: hidden to the parent container to create a sharp edge
- Blurred glow effect example:
<style> .wrapper { position: relative; } .gradient { position: relative; width: 200px; height: 200px; border-radius: 50%; background-image: linear-gradient(deeppink, red, coral gold, white); } .blurry { position: absolute; filter: blur(40px); transform: scale(1.3) translateX(10%) rotate(30deg); } .regular { filter: drop-shadow(0px 0px 25px hsl(0deg 0% 0% / 0.3)); } </style> <div class="wrapper"> <div class="gradient blurry"></div> <div class="gradient regular"></div> </div>
Backdrop Filters
-
Consider adding a card in front of a photo, then blurring the card to obfuscate a portion of the photo
-
The property backdrop-filter will blur everything behind the element
.blur-circle { backdrop-filter: blur(10px); /* Vendor prefix for Safari */ -webkit-backdrop-filter: blur(10px); }
-
backdrop-filter supports the full range of filter functions, not just blur
Border Radius
-
To summarise, border-radius is a shorthand for 4 distinct CSS properties
- border-top-left-radius
- border-top-right-radius
- border-bottom-left-radius
- border-bottom-right-radius
-
Each property accepts two values, the horizontal and vertical radius
-
Consider an ellipse at each corner, allowing peculiar curvatures to be created
- The first value is used as the horizontal radius
- The second value is used as the vertical radius
-
When we assign a single value to border-radius, technically, it's being used 8 times
-
When using a percentage value, horizontal / vertical radii are based on width and height respectively
- Given an element of 300px by 150px, it has an aspect ratio of 2:1
- Assigning a single percentage value to border-radius produces an ellipse with the same aspect ratio
- Setting 50% means the diameter is 100%, so the ellipse is the same width / height as the element itself
- The resulting shape becomes an ellipse
-
All 8 values can be set to border-radius with the / delimiter
- The first 4 numbers are horizontal radii, clockwise order from top-left
- The last 4 numbers are vertical radii, clockwise order from top-left
- If only the first 4 numbers are provided, they're copied to the last 4
.box { border-radius: 10% 20% 30% 40% / 50% 60% 70% 80%; }
-
Blobby shapes can be created by utilising all 8 values
- Tool for visualising custom blob shapes
Nested Radiuses
-
A common mistake occurs when there's a rounded element in a rounded container
- Both parent and child elements have the same border-radius
- But, the corners look chunkier in the middle due to each corner being rounded according to their own ellipse
- The corners would look more consistent if they shared the same centre point, to ensure even thickness
-
Need to figure out the radius of the larger circle
- Sum up inner circle's radius, padding or any other spacing
.parent { --inner-radius: 16px; --padding: 8px; border-radius: calc(var(--inner-radius) + var(--padding)); padding: var(--padding); } .child { border-radius: var(--inner-radius); }
-
If the outer radius is known and we want to calculate the inner radius
- Perform same calculation but subtract the padding instead of adding it
Circular Radius
-
Produce a dynamically-sized rounded object where its corners maintain symmetry and a circular shape (doesn't stretch into ovals)
- Given the exact width and height of the element, set border-radius to half that amount
- If the container size is dynamic, a hacky fix is to set a very large number
.card { border-radius: 5000px; }
-
The border-radius algorithm has a built-in limit, not allowing the corners to grow so large that they run into each other
- Given an element of 200px by 100px, each corner has a maximum radius of 50px
-
Similarly, asymmetric circles can be created using a similar technique
- The behaviour is described in the specification, and implemented by all browsers
.card { width: 200px; height: 100px; border: 3px solid; border-radius: 5000px 5000px 1000px 1000px; }
-
Alternatively, instead of selecting an arbitrary enormous number, use 100vmax
- vmax unit switches between vw or vh depending on the larger one
- Setting 100% of the viewport's largest size guarantees it'll be bigger than the element could be
- Given the element is smaller than the viewport itself
- Pretty much the same as setting border-radius: 10000px but feels less hacky
.card { border-radius: 100vmax; }
Shadows
-
Three main ways to apply shadowsL box-shadow, text-shadow and filter: drop-shadow
-
box-shadow is the most common way
- Based on a box model, the element's box will cast a simulated shadow behind it
- Commonly called with 4 arguments: horizontal offset, vertical offset, blur radius, colour
-
filter: drop-shadow
- drop-shadow uses Gaussian blurring under the hood, producing a softer, blended shadow compared to box-shadow
- Accepts the same values and types as box-shadow, but the third argument is not blur radius, it's standard deviation
-
text-shadow applies only to the typography within the selected element
- A common use case is to increase contrast between light-coloured text and a light background
p { text-shadow: 2px 4px 8px hsl(0deg 0% 0% / 0.25); }
Contoured Shadows
- When using filter: drop-shadow on an image supporting transparency (png, gif, svg), the shadow applies to non-transparent parts of the image
- Works on DOM nodes, not just limited to images
- Applying filter: drop-shadow to an element contours its descendants as well
- Can apply to a group of elements, ensure no shadow overlap which occurs with box-shadow on tightly-grouped siblings
.grid { filter: drop-shadow(2px 4px 32px hsl(0deg 0% 0% / 0.4)); }
Singled-Sided Shadows
-
box-shadow allows shadows to be cast in a single direction, rather than spilling out in all directions
- It takes an optional fourth argument for the spread radius, allowing the shadow size to increase or decrease
- A spread of 2px means the shadow grows by 2px in every direction, increasing the overall size by 4px wider and taller
/* 10px is the fourth argument */ .box { box-shadow: 0px 2px 4px 10px red; }
-
Shadows spill out everywhere due to blur radius, spread radius can be used to offset this growth
- The following example displays the shadow on the bottom edge
- Tweak the first two values of box-shadow to change the direction
.box { --blur: 8px; --spread: calc(var(--blur) * -1); --offset: 12px; box-shadow: 0px var(--offset) var(--blur) var(--spread) hsl(0deg 0% 0% / 0.2); }
Inset Shadows
- box-shadow can be used to create inner shadows
- Creates an illusion that the element is lower than the surrounding environment
.wrapper { box-shadow: inset 2px 2px 8px hsl(0deg 0% 0% / 0.33); }
Designing Shadows
-
The direction of shadows should be consistent across all elements
-
In real life, shadows are cast from a light source
- Therefore, we should decide on the position of the "light source" for the elements of the page
-
Every shadow on the page should share the same ratio, creating an effect where every element appears to be lit from a common light source
-
To adjust elevation as well, all four variables of box-shadow must be adjusted
- The horizontal / vertical offset scale together at a specific ratio
- Blur radius gets larger as elevation increases
- Shadow becomes less opaque as elevation increases
-
Can create a slightly more realistic shadow by layering multiple shadows on top of one another
- Each has slightly different offsets and radii, bit more computationally expensive
.layered.box { box-shadow: 0 1px 1px hsl(0deg 0% 0% / 0.075), 0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075), 0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075); }
-
Find the right colour to match background and shadow by matching the hue and lowering saturation / lightness
- Creates a shadow that's not too desaturated but also not desaturated enough
Colors
Accessibility
-
Two questions to ask ourselves when using colour in UI
- Is there enough contrast between the text/UI and background?
- Can folks who are colour-blind understand this UI?
-
There needs to be significant amount of contrast between text and its background for it to be legible
-
Standardised way of checking contrast levels: WCAG contrast ratios
- Formula accepting two colours, and returns a number. The bigger the number, the greater the contrast
- Scale ranges from 1 (no contrast) to 21 (maximum contrast)
-
The minimum acceptable contrast ratios:
- AA guidelines:
- Normal text: 4.5
- Large text: 3
- AAA guidelines:
- Normal text: 7
- Large text: 4.5
- AA guidelines:
-
Color Review tool to check colour contrast for any two colours
-
Color picker browser extension to infer the colour of any pixel on a page
-
Types of colour blindness:
- Red / green (protanopia, deuteranopia)
- Blue / yellow (tritanopia)
- Complete colour blindness (monochromacy)
-
Becareful not to rely on colour to communicate meaning
- Example: Red for failure, green for success
- Should be treated as a secondary indicator
Selection Colors
-
By default, a blue background is applied when selecting text
-
Can tweak the background and text colour for the selection box using the ::selection pseudo-element
::selection { color: hsl(25deg 100% 20%); background-color: hsl(55deg 100% 60%); }
-
Selection styles aren't inheritable, consider the following example:
- The emphasized text uses the default styling rather than the paragraph-specific ones
<style> p::selection { color: black; background-color: yellow; } </style> <p> This paragraph has <em>some emphasized text</em>, in the middle. </p>
- The work around is using CSS variables, define global defaults in html
- Can re-assign the values for specific elements that need to override the styling if necessary
<style> ::selection { color: var(--selection-color); background-color: var(--selection-background); } html { --selection-color: black; --selection-background: yellow; } </style> <p> This paragraph has <em>some emphasized text</em>, in the middle. </p>
Accent Colors
- accent-color property allows us to change the colours used in a handful of HTML elements
- Different types of UI are more easily customisable
- Example: Checkboxes, radio buttons, sliders, etc.
Gradients
Linear Gradients
-
The function linear-gradient takes an arbitrary amount of colours and interpolates between them starting from the top and going down
-
Passing an optional angle as the first argument allows the gradient to run at a different angle
- The default angle is 180deg, therefore, 0deg makes the gradient run from bottom to top
- Cardinal directions can also be specified with the to keyword
.box { background-image: linear-gradient(to right, deeppink, gold, coral, white); }
-
Gradients have "colour stops" which are points along the spectrum where the colour is fully applied
- By default, the colours are equidistant
- Their placement can be modified using percentages
.box { background-image: linear-gradient( deeppink, gold 10%, coral 30%, white ); }
- Can create sharp lines by positioning stops very close together, here's a brief snippet:
deeppink 0%, deeppink 9.99%, red 10%, red 19.99%, ...
-
Colour hints allow us to move the midpoint between two colours
.box { background-image: linear-gradient(deeppink, 20%, gold); }
Radial Gradients
-
A radial gradient emanates outwards in a circle from a single point
.box { background-image: radial-gradient(deeppink, gold, coral, white); }
-
It can also take in an optional first argument
- A bit weird, works like a position and sort of a size
- In the snippet, the gradient's "core" expands half the available width and the full height, creating a tall and skinny ellipse
.box { background-image: radial-gradient(50% 100%, deeppink, gold, coral, white); }
-
Can "lock in" the shape using the circle at prefix to create a sunset effect
.box { background-image: radial-gradient( circle at 50% 100%, white 0%, yellow 10%, gold 20%, coral 30%, skyblue ); }
Conic Gradients
-
Conic gradients are the result of taking a linear gradient and forming it into a circle
- Depending on the colours chosen, there will be a distinct contrast at the point where the start and end points meet
.box { background-image: conic-gradient(deeppink, red, gold); }
-
To smooth out the harsh vertical line, just make the start and end colours similar to create a smoother effect
.box { background-image: conic-gradient(deeppink, red, gold, red, deeppink); }
-
Can also make all the lines harsh by employing the same technique used in linear gradients for creating sharp lines
-
Linear gradients are given an angle (eg. 45deg), radial gradients are given a position (eg. circle at 50% 100%), conic gradients take both
- The format is from <angle> at <position>
Easing Gradients
- Can choose a timing function for gradients, just like for motion
- Adjust how the colours are mixed in-between, the start and end colours remain constant
- Web-based tool to generate CSS to simulate different timing functions
Gradient Dead Zones
- Sometimes, gradients get a bit gray in the middle
- Occurs because it will try take the shortest route between two colours using RGB values on the colour wheel
- If we took a curved route around the colour wheel, it will result in a nicer gradient
- Can be achieved by picking saturated midpoint colours, or use the Gradient Generator tool
Mobile UX Improvements
-
When tapping interactive elements on mobile devices, the browser will flash a "tap rectangle" briefly
- Can remove with this declaration:
.pushable { -webkit-tap-highlight-color: transparent; }
-
Holding down interactive elements can cause text within it to select
- Can make elements unselectable with user-select property
.pushable { user-select: none; }
-
A mobile input device is the touchscreen, which is a coarse pointer
-
It's hard to be precise with a coarse pointer, Apple recommends a minimuim tap target size of 44px by 44px
-
Don't have to make the element appear bigger, use a pseudo-element that extends outwards by some length in every direction
- Ensure link / button is a containing block for positioned elements (position: relative)
- Add a child pseudo-element, make it positioned absolutely
- Set top / left / right / bottom to a negative value so it textends outwards
Pointer Events
- Sometimes, a parent container will be too large which makes interactive elements behind it unclickable
- pointer-events property allows us to set an element to be a hologram, can't trigger a click event
- Can still focus the button using the keyboard or select text
button { pointer-events: none; }
Clipping With clip-path
-
clip-path property allows us to trim a DOM node into a specific shape
.triangle { width: 100px; height: 80px; background-color: deeppink; clip-path: polygon( 0% 100%, 50% 0%, 100% 100% ); }
-
Has no effect on layout, take up the same amount of space in the DOM
-
The polygon function relies on a coordinate system
- Common to use percentages
- The origin is the top-left corner
Animations
- clip-path can be animated using transition
- Only works if the polygon definitions have the same number of points
- Each point is interpolated in the order they're provided
- To animate elements with different number of points, need to cheat by adding multiple points to the same spot
- The following example displays a square that transitions to a triangle when hovered
.triangle { clip-path: polygon( 0% 0%, 100% 0%, 100% 100%, 0% 100% ); transition: clip-path 250ms; will-change: transform; } .triangle-wrapper:hover .triangle, .triangle-wrapper:focus .triangle { clip-path: polygon( 0% 0%, 100% 50%, 100% 50%, 0% 100% ); }
Rounded Shapes
- clip-path supports both circle and ellipse function
- The template syntax is below:
.wrapper { clip-path: ellipse( xRadius yRadius at xPosition yPosition ) }
With Shadows
- When using clip-path and filter together, the filter is applied before clip-path
- Resolved by moving filter to the parent element
.wrapper { filter: drop-shadow(1px 2px 4px hsl(0deg 0% 0% / 0.5)); } .triangle { clip-path: polygon( 0% 100%, 50% 0%, 100% 100% ); }
Optical Alignment
-
Optical alignment is to align things based on their perceived symmetry, rather than based on mathematical values
-
Consider a heading in a container with padding
- The padding is uneven on each side because it's not measuring from the characters, but the text selection box itself
-
Use space tokens which correspond to specific pixel / rem values
- Ensure consistent spacing across an application
-
Utility component for shifting elements around, such as the number in a notification badge
function ShiftBy({ x = 0, y = 0, children, as = "div", style = {}, ...delegated }) { const Element = as; return ( <Element style={{ transform: `translate(${x}px, ${y}px)` }} {...delegated} > {children} </Element> ); }
<Notifications> <ShiftBy y={1}> {numOfNotifications} </ShiftBy> </Notifications>
Scrolling
Smooth Scrolling
-
Consider a table of contents, when a link is clicked it teleports you to the specific location
-
This process can be smoothed out using a single declaration
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }
-
Can't tweak the duration or easing of the scroll animation, up to browser and operating system
-
Can control scroll position from within JavaScript
- The following snippet scrolls the user to the top of the page when they submit a form
- window.scrollTo inherits the behaviour of scroll-behavior: smooth added to the html tag
function handleSubmit(event) { window.scrollTo(0, 0); }
- Can also specify scroll behaviour using the long-form version
const prefersReducedMotion = getPrefersReducedMotion(); window.scrollTo({ top: 0, left: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth' });
-
scrollIntoView method allows us to scroll to a particular element
const prefersReducedMotion = getPrefersReducedMotion(); const target = document.querySelector('.title'); target?.scrollIntoTo({ behavior: prefersReducedMotion ? 'auto' : 'smooth' });
-
There may be different behaviours in single-page applications such as a React app
- Disable page-wide smooth scrolling and opt into a case-by-case basis
- Create a component that uses smooth scrolling for specific links (returns <a>), uses usePrefersReducedMotion
Scroll Snapping
-
Consider swiping side-to-side to different pages
- When the user stops scrolling, the scroll should automatically shift so the nearest element is fitted to the screen
- This design is created by using a container element and filling it with full-width elements
.wrapper { display: flex; height: 100%: overflow: auto; } .box { min-width: 100%; }
-
Two primary scroll-snap properties:
-
scroll-snap-type controls the direction and precision of the scroll snapping
- mandatory means the element will always snap
- proximity is more subtle, only triggers a snap when the user is near a snap point (determined by browser)
-
scroll-snap-align controls which part of the child we want to snap
.wrapper { scroll-snap-type: x mandatory; } .box { scroll-snap-align: start; }
-
Scrollbar Colors
-
Scrollbars are made up of at least 2 components, the thumb and the track
- Each OS has its own scrollbar
-
scrollbar-color property allows us to tweak scrollbar colours
- The first colour is for the thumb, the second is for the colour of the track
body { scrollbar-color: color1, color2; }
-
Can also use the background-color property on global pseudo-elements
- Other CSS properties can be used inside these pseudo-elements
- By using border and border-radius, you can create a scrollbar that looks similar across all browsers
::-webkit-scrollbar { background-color: color2; } ::-webkit-scrollbar-thumb { background-color: color1; }
-
Styling scrollbars for mobile devices is subjective, but to remove it, just wrap in a media query
- There's no "mobile devices only" media query, so setting a window size is the best option despite potentially affecting some desktop users
@media (min-width: 500px) { html { scrollbar-color: ... ; } ::-webkit-scrollbar { ... } ::-webkit-scrollbar-thumb { ... } }
Scroll Optimization
-
Consider a fixed or sticky element that hides the heading of a link you clicked
-
Use the scroll-margin-top property to simply resolve this issue
- The distance that an element should sit from the top of the viewport when scrolled into view is defined on the property
h2 { scroll-margin-top: 6rem; }
-
The Cumulative Layout Shift (CLS) metric is a measure of how much movement is on a page as it loads in
-
It's important to optimise for CLS
- Layout shifts are jarring and chaotic
- Google incorporated CLS into its search ranking algorithm, so focusing on CLS improves search engine optimisation
-
Ensure there's always a scrollbar, even if the page isn't tall enough to warrant one
body { overflow-y: scroll; }
-
Images should be a fixed size, providing at least two of the CSS properties
- width, height or aspect-ratio
- If there is no other choice, use object-fit: cover
- Don't want images defaulting to 0px wide and 0px tall, it causes a big layout shift when the image loads
-
If there are multiple sections to load, the experience can be improved by grouping things together
- Such as waiting for all elements to load before showing any of them
- The tradeoff is reduced number of layout shifts, but the user won't see anything until everything is loaded
Focus Improvements
Focus Visible
-
At any given moment, exactly one element on the page is considered "active" or focused
-
Focus can be shifted between elements by:
- Hitting Tab key
- Clicking an interactive element
-
When an element is focused, they are often represented with an outline as an indicator
- The pseudo-class :focus-visible is similar to :focus but only matches when:
- The element is focused
- The user is using a non-pointer input device
outline: 5px auto -webkit-focus-ring-color;
- The pseudo-class :focus-visible is similar to :focus but only matches when:
Focus Within
-
Use child selectors to style a descendant when an ancestor is focused
<style> a:focus .highlighted { background: hsl(55deg 100% 75%); } </style> <a href="/"> Focus me and <span class="highlighted">see the magic!</span> </a>
-
If we wanted to apply a style to the parent when a child is focused, use the :focus-within pseudo-class
Focus Outlines
-
Applying outline styles to a non-interactive element like <div> or a <p>, it acts like a border
-
However, when focusing on an interactive element, it's treated to a browser-specific "focus outline"
-
Focus outlines can be customisable but they don't behave like typical outlines
- Here, outline-color has no effect on an interactive element like the <a> tag
a { outline-color: red; }
- outline-color is broadly supported but only for typical outlines
- The following snippet shows how to add outlines for an interactive element
a:focus { outline: 2px solid red; }
Floats
-
Floats can specify a shape for the text to wrap around
- shape-outside can accept shapes from functions like circle or clip-path
- Other alternatives are SVG-style paths for complex shapes or auto-detection by passing a URL to an image with transparency
.floated { float: left; shape-outside: circle(); margin-right: 24px; }
-
Floats can break your layout
- Floated elements are taken out of flow
- The wrapper element ignores it which can cause the image to spill out since the container doesn't grow with it
-
clear is a legacy CSS property that allows us to place additional content below a floated element, in case we don't want to wrap around it
<style> .clear { clear: left; } </style> <div class="wrapper"> <img class="floated" src="URL" alt="" /> <p> Text. </p> <p class="clear"> This paragraph appears below the image. </p> </div>
-
Alternatively, an empty element that clears floats will cause the parent to grow to contain it, this is a clearfix
<style> .clearfix::after { content: ""; display: block; clear: both; } </style> <div class="wrapper"> <div class="clearfix"> <img class="floated clearfix" src="URL" alt="" /> <p> Text. </p> </div> <p class="clear"> This paragraph appears below the image. </p> </div>