Graduate Program KB

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
  • 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;
    

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>