Skip to content

Articles

Long form articles from Arpit Agrawal, a web designer-developer from India.

  • Posted on

    Safari drops support for the theme-color meta tag

    The <meta name="theme-color"> tag is no longer supported in Safari 26 on macOS and iOS.

    If a fixed or sticky element touches the top or bottom edge of the window, then and only then, Safari extends that element’s background color into the corresponding top or bottom bar. Otherwise, on iOS, the bars remain translucent and have no solid color background.

    As Wenson Hseih explained on WebKit Bugzilla:

    A solid background color extension (“top bar tint”) is only needed in cases where there’s a viewport-constrained (fixed or sticky) element near one of the edges of the viewport that borders an obscured content inset (such as the top toolbar on macOS, or compact tab bar on iOS), in order to avoid a gap above or below fixed elements in the page that would otherwise appear when scrolling. This color extension behavior is more critical on iPhone, where there’s a much “softer” blur effect underneath the browser UI (and so more of the page underneath is otherwise directly visible).

  • Posted on

    Sorting strings that contain numbers in JavaScript

    Recently, I was reading Jan Miksovsky’s article on how he built a basic static site generator using plain JS with zero dependencies.

    While exploring the source code, I noticed the following bit of code:

    // For sorting in natural order
    const naturalOrder = new Intl.Collator(undefined, {
    numeric: true,
    }).compare;

    I was totally unaware of the Intl.Collator API and that it can be used for sorting. This sent me down a bit of a rabbit hole.

    I learned that my mental model of how JavaScript’s default array sort() works is wrong. I also learned a counter-intuitive detail about its behavior with numbers. But, most importantly, I learned about better ways to handle sorting correctly in JavaScript, including the neat Intl.Collator API that Jan’s code led me to discover.

    Consider the following array of numbers:

    const array = [9, 8, 7, 60, 5];
    array.sort();

    Now, what do you think is the result?

    As someone who is not a JavaScript expert, I thought the output would be the following:

    Terminal window
    [5, 7, 8, 9, 60]

    I was wrong. The correct output is the following:

    Terminal window
    [5, 60, 7, 8, 9]

    Now, it was important to understand why I was wrong.

    The default sorting behaviour

    By default, when no compare function is provided, the sort() method converts each element into a string and then compares these strings character by character based on their sequences of UTF-16 code unit values. (JavaScript strings use the UTF-16 character encoding).

    When we sort the array in my example using the default sort(), the numbers are first converted to strings. Then, the comparison starts with the UTF-16 code unit value of the first character of each string. The character ‘6’ in ‘60’ has a UTF-16 code unit value of 54, which is lower than the code unit values for ‘7’ (55), ‘8’ (56), and ‘9’ (57). The rest of the string (‘0’ in ‘60’) is only considered if the first characters are the same. This is why ‘60’ appears before ‘7’, ‘8’, and ‘9’ in the sorted output.

    "5".charCodeAt(0); // Output: 53
    "60".charCodeAt(0); // Output: 54
    "7".charCodeAt(0); // Output: 55
    "8".charCodeAt(0); // Output: 56
    "9".charCodeAt(0); // Output: 57

    Sorting Numbers Correctly

    If you are specifically sorting an array that contains only numbers, the standard way to achieve correct numeric sorting is to provide a simple compare function:

    const numbersArray = [9, 8, 7, 60, 5];
    numbersArray.sort((a, b) => a - b);
    // Output: [5, 7, 8, 9, 60]

    This compare function subtracts b from a. If the result is negative, a comes first. If positive, b comes first. If zero, their order doesn’t change relative to each other. This correctly orders numbers based on their mathematical value.

    Natural Sorting with the Intl.Collator API

    However, the simple (a - b) approach works reliably only for pure numbers. What about sorting strings that contain numbers, like filenames? This is where the Intl.Collator API with the numeric: true option shines. It provides the natural sorting behavior that correctly handles numbers alongside other characters.

    const naturalOrder = new Intl.Collator(undefined, {
    numeric: true,
    }).compare;
    // Note: Passing `undefined` as the locale uses the default locale of the browser's runtime.
    // You could specify a locale string like 'en-US' here if needed.
    const array = [9, 8, 7, 60, 5];
    array.sort((a, b) => naturalOrder(a, b));
    // Output: [5, 7, 8, 9, 60]

    By passing the compare method of the Intl.Collator instance to sort(), we override the default string comparison. The Intl.Collator object, configured with numeric: true, knows how to compare ‘60’ and ‘7’ such that 60 is treated as a single number and correctly placed after 7, 8, and 9.

    This API is incredibly useful for sorting strings that contain numbers in a human-expected way, such as lists of filenames like IMG_1.png, IMG_2.png, IMG_10.png, IMG_20.png.

    const files = ["IMG_2.png", "IMG_1.png", "IMG_10.png", "IMG_20.png"];
    files.sort();
    // Output: ['IMG_1.png', 'IMG_10.png', 'IMG_2.png', 'IMG_20.png']
    files.sort((a, b) => naturalOrder(a, b));
    // Output: ['IMG_1.png', 'IMG_2.png', 'IMG_10.png', 'IMG_20.png']

    How numeric: true works under the hood

    When numeric: true is set, the collator detects numeric substrings inside the strings and parses those substrings as actual numbers, not as sequences of digits. And then it compares the numbers numerically, not character by character.

    Conclusion

    For simple numeric arrays, (a, b) => a - b is your go-to. For more complex natural sorting needs with strings containing numbers, the Intl.Collator API provides a robust and locale-aware solution.

  • Posted on

    Why is `grid-row: 1/-1` not working?

    The grid-row grid-placement property is shorthand for grid-row-start and grid-row-end.

    .grid-item {
    grid-row: 1/-1;
    /* is equivalent to */
    grid-row-start: 1;
    grid-row-end: -1;
    }

    In the above declaration, we use integers to position and size the grid item by line numbers.

    As per the spec:

    Numeric indexes in the grid-placement properties count from the edges of the explicit grid. Positive indexes count from the start side (starting from 1 for the start-most explicit line), while negative indexes count from the end side (starting from -1 for the end-most explicit line).

    The important bit is the explicit grid. This begs the question …

    What is the explicit grid?

    As per the spec:

    The three properties grid-template-rows, grid-template-columns, and grid-template-areas together define the explicit grid of a grid container by specifying its explicit grid tracks.

    Simply put, the explicit grid consists of manually defined rows and columns.

    The size of the explicit grid is determined by the larger of the number of rows/columns defined by grid-template-areas and the number of rows/columns sized by grid-template-rows/grid-template-columns. Any rows/columns defined by grid-template-areas but not sized by grid-template-rows/grid-template-columns take their size from the grid-auto-rows/grid-auto-columns properties. If these properties don’t define any explicit tracks the explicit grid still contains one grid line in each axis.

    That last bit is what leads to line -1 being the same line as 1 because the explicit grid still contains one grid line in each axis.

    What is the implicit grid?

    As Manuel Matuzovic puts it:

    If there are more grid items than cells in the grid or when a grid item is placed outside of the explicit grid, the grid container automatically generates grid tracks by adding grid lines to the grid. The explicit grid together with these additional implicit tracks and lines forms the so called implicit grid.

    Conclusion

    Paraphrasing Jen Simmons:

    • -1 is the last line of the explicit grid
    • If you haven’t defined any explicit rows, then all your rows are implicit
    • For implicit rows, -1 is the same line as 1
    • Define explicit rows and grid-row: 1/-1 will work as expected

    Further Reading

  • Posted on

    Transparent borders

    In web development, small design decisions can have a significant impact on accessibility and user experience. One such decision is how we handle borders on interactive elements.

    The problem with border: none

    When styling interactive elements like buttons, it’s common practice to remove default borders using border: none. However, this approach can lead to accessibility issues, especially in high contrast mode. As demonstrated in the image below, removing the border entirely can cause buttons to appear as floating text on the page, making it difficult for users with low vision to distinguish interactive elements.

    Side-by-side comparison of the contact form on Slae.app. The left image shows the contact form with forced colors disabled, displaying the default color scheme. The right image shows the contact form with forced colors enabled. In the right image, the submit button appears as floating text on the page.

    Dave Rupert explains the importance of the default border and why it exists:

    In the case of interactive form controls (inputs, textareas, buttons, etc.), those pesky borders were put there because they have an accessibility benefit when using High Contrast Mode, a feature used by 30.6% of low-vision users.

    The transparent border solution

    To address this issue, Dave recommends making the border or outline transparent instead of removing it entirely. This can be achieved with the following CSS:

    button {
    border-color: transparent;
    }

    As demonstrated in the image below, this approach is effective for several reasons. First, sighted users will not notice the difference. Second, as Kilian Valkhof explains, in forced color mode, the border color or outline color “will be overwritten with the current text color, making it nicely visible again without needing any special adaption or re-styling for forced color mode.”

    Side-by-side comparison of the contact form on Slae.app with the transparent border solution applied. The left image shows the contact form with forced colors disabled, displaying the default color scheme. The right image shows the contact form with forced colors enabled. In the right image, the submit button appears as a button.

    User experience benefits

    Using transparent borders offers additional benefits for user experience. Consider hover effects, for example.

    button {
    border: none;
    }
    button:hover {
    border: 2px solid navy;
    }

    In such situations, applying a visible border on hover can inadvertently change the element’s dimensions. This change in size can result in a jarring visual effect.

    By setting a transparent border in the default state, we ensure smooth transitions and consistent element sizes across different states.

    <div>
    <button class="no-border-btn">Button with no border</button>
    <button class="transparent-border-btn">Button with transparent border</button>
    </div>
    .no-border-btn {
    border: none;
    &:hover {
    border: 2px solid navy;
    }
    }
    .transparent-border-btn {
    border: 2px solid transparent;
    &:hover {
    border-color: navy;
    }
    }

    Implications for design systems

    Transparent borders are also valuable in the context of themeable design systems. Brad Frost elaborates:

    When supporting multiple theme, it can be common for some themes to use borders while others don’t. This flexibility is great! Each brand is able to express themselves how they see fit. But if implemented using different border widths, shifts in the box model happen.

    By using border-color: transparent for themes without visible borders, designers and developers can maintain consistent element sizes across different variants and themes. This approach provides the flexibility to adapt the visual design while preserving the underlying structure and layout of the components.

    Conclusion

    Implementing transparent borders in your CSS addresses crucial accessibility concerns, enhances user experience across different display modes, and provides the flexibility needed for robust, adaptable design systems.