Hajime, the duck guy

Monday, November 25, 2024, by Hajime Yamasaki Vukelic

About this series

Step-by-step guide for building applications using nothing but browser APIs – no build, no tools, no frameworks

  1. Introduction to vanilla, goals, principles
  2. Requirements, architecture, content structure
  3. SVG spritesheet for icons
  4. Data molding, HTTP requests, basic event handling
  5. Storage, advanced event handling
  6. MPA, teamwork and shared code, separation of concerns
  7. Webfonts, asset preloading, responsive grid layouts
  8. Responsive tables using a grid layout with subgrids
  9. Toolless lazy loading and micro-frontends (part 1)
  10. Toolless lazy loading and micro-frontends (part 2)
  11. Reverting back to classic MPA and comparison with micro-frontend
  12. Code cleanup and vanilla CSS-only view transitions
  13. Accessible tabbed interface and a simple feature flag
  14. Visualization using vanilla SVG (part 1)
  15. Visualization using vanilla SVG (part 2)
  16. Visualization using vanilla SVG (part 3)
  17. PWA and offline mode

Requirements, architecture, content structure

Let me show you how I'd build a simple vanilla weather app. In the previous part, I mentioned there are multiple approaches, and one of them is the 'hard mode'. In this series, I'll show you the hard mode. This gives me the most opportunities to showcase various principles of how I go about working on vanilla apps. I will try to explain things in a way that lets you cherry pick anything you like if you don't want to go all-in.

What's it do?

Before I build anything, let me jot down some notes about what I want this app to do:

  • Let me pick my location by typing in the name
  • Let me quickly select from up to 5 previously used locations, with the most recent one at the top of the list
  • Let me delete any of the previously used locations so it doesn't appear on the list
  • Let me see the current weather condition: weather, temperature, humidity, precipitation probability, atmospheric pressure, wind direction and intensity
  • Let me see the summary of today's weather: max/min temperature, max precipitation probability, wind intensity
  • Let me see the forecast for the next 5 days with the same summary information as today's summary (this list can be merged with today's summary)
  • Let me use the app offline: show the last-fetched data with a warning
  • Let me add the app to my phone's home screen

I think that ought to be enough for now.

For the weather data, I'll use the Open-Meteo API. It conveniently provides a geolocation API as well, which I can use for searching locations. Plus I've used it before so I'm familiar with what I can expect.

Contexts

Every time I develop an app, I first identify the major contexts it has. A context is a topic that groups together related activities. For instance, "select a location" is a context that groups activities like "search for a location", "pick a previously used location", etc. "See weather info" would group activities like "see a weather summary", "see a 5-day forecast", and so on.

In some cases, it can be tricky to determine whether an activity belongs to a context or not. When in doubt, ask yourself whether there's a need to perform that activity from another place (e.g., "Do I need to search for a location while viewing the weather info?"). If you don't, then the activity belongs to a specific context.

In general, my opinion is that an application that is strict about compartmentalizing activities into well-defined contexts is easier to use, as it lets the user focus on a particular task and helps them not get distracted by unrelated things.

This app has two well-defined contexts. The location selection and weather information.

Moving between contexts

Once I have the contexts, I need to figure out a way to move between them and what information must be passed from one to another. For now, a rough sketch is enough. I will work out the details later.

To go from location selection to weather information, I will need the location coordinates (this is something I got from reading the API documentation) and the location name. But here's the catch. I could also land on the weather info when I start the app without visiting the location selection first. In that case, I would like to see the last location used. I need to keep the information about the "currently selected location" somewhere where it can be accessed by the weather info context at any time. It's probably simplest to just store it in localStorage.

Moving in the opposite direction – from weather info to location selection – will be done for two reasons. Firstly, I land on the weather info and there's no currently selected location. This is the default state. Secondly, if I want to switch locations. The location context does not need any information from the weather info, so for now I think I can get away with just switching to it without doing any extra work or storing any data. I'm thinking about whether I need to clear the currently selected location in localStorage when switching contexts, but I don't think so. Keeping the information would probably allow me go back to the weather info without selecting a new location if I change my mind.

Multi-page application

The first thing I do when I start working on an app (well, apart from all of the above) is I create skeleton pages. Because this is a blog post about vanilla aimed at people who may not realize what vanilla could do for them, I'm going to make a small digression here and talk about pages for a second.

Today, we have two general styles of apps:

  1. SPA
  2. Hybrid SPA

The SPA (single-page application) is a style where all contexts are shoved into a single page. Context switching is done by simulating navigation client-side, and that involves some kind of routing mechanism to select the appropriate context based on the URL and URL changes. This is what 2nd generation front-end frameworks do (e.g., React, VueJS, Svelte, Angular).

The hybrid SPA approach uses server-rendered pages as the starting point and then they are hydrated (fancy word for "JavaScript starts executing") with a boatload of JavaScript on the client-side to convert the app to a SPA – subsequent navigation works just like in a SPA. The server knows about the possible client states and renders the starting point based on the URL the same way the client-side code would. The rendering can be done ahead of time (sever-side generation, SSG) or in real-time (sever-side rendering, SSR) If that sounds wasteful... it is. This is done by the 3rd generation frameworks (e.g., NextJS, Nuxt, Astro, Remix, SvelteKit).

In the case of this weather app, I'm going to pick the third approach: MPA (multi-page application). A long time ago, this was the default. Nowadays you don't see it that often. I think it's better now than ever before, and it should reclaim its position as the default. My hidden political agenda aside, it's just a lot easier for me to work this way overall and it fits the whole "take advantage of the platform" theme. Users don't seem to mind, so I see no reason to categorically reject it.

Each context gets a separate page, and switching the context is done using the browser's built-in navigation mechanisms such as links and forms, and built-in navigation APIs.

The reasons I default to MPA are the following:

  • I don't need to worry about routing
  • MPA provides a natural separation in the code that results (or should anyway) in small well-defined contracts between pages
  • I don't have to worry about tearing down state between page transition

I'm aware that there are developers who get insulted by the very suggestion to use more than one physical page for an app and start yelling about the blanking of the page during page transition. If that's you, here's what I have to say to you: if blanking of the page during page transition is the biggest problem you have, congratulations! You don't need my advice for anything. On a more serious note, the blanking of the page can be solved with view transitions.

The bigger issue with MPA is that if you have to keep lots of state between pages, then you have to jump through hoops to get that. It's better to just go SPA if that's the case. (Or at least reconsider how you slice your contexts.)

As it's commonly the case, there are no perfect solutions. You can only skirt around drawbacks so that a minimum number of them apply to you. Though, hybrid SPA is most definitely a horrible solution waiting for a problem to emerge. Want my advice? Stay the fuck away from it.

Ok, that's enough digression.

Skeleton pages

Let me go back to the skeleton pages. I have a template in my IDE that generates code like this when I expand ! with autocompletion.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link rel="stylesheet" media="screen" href="index.css">

    <script type="module" src="index.js"></script>

    <title>Document</title>
</head>
<body>

</body>
</html>

A few things to note here:

  1. Always include the viewport meta tag as its absence will skew the styling on mobile device screens.
  2. Always include the media attribute for your stylesheet links. This ensures that only the targeted devices fetch the stylesheet (e.g., if you say media="print", the stylesheet won't be fetched by non-printer devices).
  3. Use either type="module" or defer on the <script> tags.

Regarding the last point, people traditionally place <script> tags at the bottom of the body. This is done so that by the time the scripts are fetched by the browser, most of the page has been parsed and ready for the script to use. The problem with this approach is that the browser has to parse the entire page before it even learns about the script. (Granted, it doesn't that that much time, but still...) By placing the <script> tag in the <head>, and marking it as defer or type="module", the browser will start fetching the scripts immediate in the background, and allow the page to be parsed at the same time, and then execute the scripts once the paring is done. Additionally, it will fetch scripts in parallel, too, but still execute them in the order in which they are listed in the HTML. In short, it doesn't get much better than this.

The difference between type="module" and defer is that the former instructs the browser to treat the script as an ES module – you can use import and export keywords, and all top-level variables are module-local, not global. As far as loading goes, though, they are identical. I therefore always use type="module" regardless of whether the script is actually a module or not.

As I mentioned before, I decided this app has two contexts, so I'm going to create two sets of files:

  • index.html
  • index.js
  • index.css
  • location.html
  • location.js
  • location.css

For now, the only thing I change in the <head> section for these files is the title. The app is going to be called A weather app so that's what I'm going to have as the title for the index page, and for the location page, the page title is going to be A weather app | pick a location.

The development server

I can now start your development server. Which one? Well, any will work. I can even open the HTML file directly in my browser, though I generally recommend using some kind of HTTP server.

I use a JetBrains IDE (did you know WebStorm is free now?) so I use the built-in preview for this. Alternatively you can use the http-server package from npm by running the following command:

npx http-server .

If you use a plain HTTP server, I also recommend the Reload CSS extension so that you can quickly reload just the CSS without reloading the whole page.

For a more serious project, I'd use Docker to get a nginx container up with the fully power of its configuration at my disposal (e.g., URL rewriting, ETag support, etc.).

The content structure

I could start with either page. It's a judgement call. Since I need a location for the weather info page, it makes sense to do the location picker first. On the other hand, I could simply edit the localStorage from within the dev tools and start with the weather info page first. This helps me work out the format in which the location selection page should store the selected location.

I'm going with the location selection page first simply because it's smaller and I'd like to wrap up before you start yawning. The first order of business is the content structure.


<body>
<header>
    <h1>
        <x-icon name="logo"></x-icon>
        A weather app
    </h1>
</header>

<main>
    <h2>Pick a location</h2>

    <x-search>
        <h3 class="alt-text">Location search</h3>

        <form>
            <label>
                <span class="alt-text">Search for a location</span>
                <input type="text" name="search" required>
            </label>
            <button>
                <x-icon name="search"></x-icon>
                <span>Search</span>
            </button>
        </form>
    </x-search>

    <x-location-list>
        <h3 class="alt-text">Location list</h3>

        <x-previously-used-locations>
            <h4 class="alt-text">Previously used locations</h4>
            <ul></ul>
        </x-previously-used-locations>

        <x-search-results>
            <h4 class="alt-text">Search results</h4>
            <ul></ul>
        </x-search-results>
    </x-location-list>
</main>
</body>

At this stage, I'm only concerned about the semantics of the markup. I'm not that worried about the appearance or behavior. This is what is meant by "separation of concerns" when (some) front end engineers talk about HTML/CSS/JS separation.

Note the heading structure. I have 4 heading levels, allowing anyone who has access to the heading list to drill down to the page content. Heading level 1 is reserved for the application name. I make it match the contents of the <title> tag, and it will be shared across both pages. The level 2 heading is the page title.

Headings also help you identify what part of the page does what. For example, if I only had two sets of <ul> in the Location list section, you wouldn't be able to tell why there are two and not just one.

Note, however, that all headings are marked as .alt-text. This is a class I normally use to define text that shouldn't be visible but still available to accessibility technologies. Think of it as an alt attribute on the image.

Elements whose tag names start with x- are custom elements. They don't do anything yet, but I add them just in case. Like headings, they can serve as convenient explainers of what each section does, but they can also serve as hooks for CSS and JavaScript later. Think of them as fancy <div>s for now.

You will notice that I don't shy away from using very long names if I can't figure out a shorter way to say it or it better matches the labels used in the UI (whether visible or otherwise). I believe removing any trace of ambiguity is better than saving a few characters while typing. Yes, it tends to add a few more KB to the payload as well, but given that I'm saving hundreds if not thousands of KB compared to the modern engineering practices, I'm more than fine with it. It will save me tons of time later when I get back to the code after a break.

There are still two pieces missing. I need templates for the respective list items. The list of previously used locations needs a list that includes a button that lets me delete the item. The list of search results doesn't need such a button. Note that I'm not even considering their appearance yet, even though realistically they would share a similar appearance. I'm still talking about what these things are in terms of their semantics.

<x-previously-used-locations>
    <!-- ... -->
    <template>
        <li>
            <button>
                <x-icon name="star"></x-icon>
                <span></span>
            </button>
            <button>
                <x-icon name="trash"></x-icon>
                <span class="alt-text">Delete</span>
            </button>
        </li>
    </template>
</x-previously-used-locations>

<x-search-results>
    <!-- ... -->
    <template>
        <li>
            <button>
                <x-icon name="pin"></x-icon>
                <span></span>
            </button>
        </li>
    </template>
</x-search-results>

Both templates have a button that looks identical except for the icon. I could have extracted that into a separate template, but I don't yet know if they are going to evolve in different directions, so I'm leaving those separate for now.

Since the templates are not very likely to be reused across different UI components, I keep them within the custom elements so they can be easily selected without requiring an id attribute or some other marker.

Styling

For the next step, I could do either CSS or JavaScript first. It's usually a bad idea to try to do both at the same time. Each approach has its pros and cons. The CSS path lets you leverage CSS as much as possible before consider any JavaScript, and it also makes for a visually more appealing development experience. The JavaScript path lets you deal with possible drastic markup changes early if you didn't get everything right in the planning phase.

In this case, I will do CSS first, as this is a relatively small app and I don't expect any big changes anyway.

As usual, I'll start with the reset. Since both pages will share the same stylesheet, I'm going to create a new CSS file, common.css, which I'll link to both pages.

<link rel="stylesheet" media="screen" href="common.css">
<link rel="stylesheet" media="screen" href="location.css">

The reset

I'll go through the reset step by step.

You will notice that these are not comprehensive resets that you can just copy/paste into all your projects. These resets are specific to the project, and I only include the parts I'm actually using. When I start a new project, I copy the reset, and then adjust the selectors according to what I expect to be doing in the project.

First I want to switch up the box model to border-box for all elements as well as the pseudo-elements. This covers 99% of the elements I need. There are some that are not covered (e.g., dialog backdrop) but from my experience this is sufficient. The important part is to not forget the ::before and ::after pseudo-elements as those are used quite often.

*, *::before, *::after {
    box-sizing: border-box;
}

Don't reset the margin and padding for * elements. Some of the default paddings and margins are actually useful (e.g., backdrop on dialogs).

Next I need to rest margins and paddings for elements I'm going to be using. In most of the cases I use the flexbox to control the relationships between elements, so margins are not needed 99% of the time. As for paddings, I prefer to define them on case-by-case basis.

html, body, h1, h2, h3, h4, ul {
    margin: 0;
    padding: 0;
}

By default, form controls do not inherit fonts and text color from the surrounding elements. We generally want them to, so we're gonna fix that. We'll also reset the font sizes and weights of our headings.

button, input, textarea, select, h1, h2, h3, h4 {
    font: inherit;
    color: inherit;
}

Contrary to common belief, the role of a heading isn't to specify text size. It's to define the level of a section. I won't go into the details here, but let me just mention that headings of the same level can have different font size (and style) and vice versa, depending on the where they are used.

Some elements are clickable but don't have the same pointer cursor style as the links. I'm going to reset them to have the same cursor style:

button {
    cursor: pointer;
}

Normally I'd also include select lists, radio buttons, checkboxes and summary elements here, but since I don't have those yet, I'm only including the button.

Last but not least, the list style reset:

ul {
    list-style: none;
}

Before I go on to the project styling, I need to add one more thing, and that's the declaration for the .alt-text class.

.alt-text {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}

This is called "visual hiding", and hides the text from the visually able while keeping it perceivable to accessibility aids such as screen readers. Using display: none or visibility: hidden doesn't work.

(People following my work know I keep saying: "Don't use pixels in your CSS. Period." Well, this is one of two exceptions. The pixels here are not visible to the end user so it's fine. Secondly, the reason I use 1px is that it's supposed to mean "the smallest non-zero value": 1px is the value that fits that description without costing tons of characters. The other exception is defining stroke width for SVG elements because internally SVG uses pixels for everything and those pixels are not the same pixels used outside of SVGs.)

Preparing for project styles

Before I do the project styles, I need to prepare a few things.

Firstly, while I did say I'd work on CSS first, and JavaScript later, there's one bit of JS (not a lot, I promise) that I need to write first, because that will affect the presentation a lot. It's the <x-icon> custom element. Since this custom element is going to be used on both pages, I need to create a common.js module first.

This module will contain the base class for all custom elements. Over the years, I've done a few things over and over again that I know I'm going to need, so I'm creating a custom base class that will house these things.

export class CustomElement extends HTMLElement {
    connectedCallback() {
        this.setup?.()
        delete this.setup
    }
}

For starters, I'm adding support for a call-once setup method. This method will implement anything that should only be done once in the custom element's lifecycle (e.g., setting up the HTML contents, setting the initial values of attributes, etc.).

The <x-icon> custom element will render a SVG that references a spritesheet I don't yet have. However, this will be enough to deal with the page layout.

customElements.define('x-icon', class extends CustomElement {
    setup() {
        this.innerHTML = `<svg><use href="icons.svg#name"/></svg>`
    }
})

Now I can import this module in location.js:

import './common.js'

Project styles

When I work on a new project, I like to start with the color scheme. This gives me an opportunity to consider the various things I might have on the page and how they will look.

I first define the custom properties using the @property rules.

The general form is:

@property --custom-property-name {
    syntax: "<color>";
    inherits: true;
}

This declares the type of the custom property values (in our case, <color>) and how the browser handles the cascade (inherits: true is normally what you want). I create one of these for each of the color-related custom property appearing in the CSS below:

:root {
    --background-color: #333;
    --secondary-background-color: #444;
    --text-color: #efefef;
    --control-color: #777;
    --control-text-color: #efefef;
    --control-highlight-color: #2f70ad;
    --control-highlight-text-color: #dbe4ea;
}

This is the dark theme. For the light theme, I will use a media query (for now):

@media (prefers-color-scheme: light) {
    :root {
        --background-color: #efefef;
        --text-color: #333;
    }
}

I've only flipped two colors, and made the rest work well with either dark or light mode. I will also include a transition between light and dark mode so that the initial transition form the white page is smoother. While at it, I will also include some of the global defaults.

:root {
    /* .... */

    --icon-size: 1em;

    background: var(--background-color);
    color: var(--text-color);
    font-family: sans-serif;
    transition-duration: 0.3s;
    transition-property: --background-color --secondary-background-color --text-color --control-color --control-text-color --control-highlight-color --control-highlight-text-color;
}

Note that I'm using the custom properties directly in transition-property without wrapping them in var(). This is because I'm specifying the names of the properties that should be transitioned, not their value. Also note that the transition only works if there's an @property declaration.

The --icon-size variable will be used by the styles for the icon custom element. Let me take care of that now.

x-icon {
    display: inline-flex;
}

x-icon svg {
    width: var(--icon-size);
    aspect-ratio: 1;
}

With the way --icon-size is declared, I can set the --icon-size custom property on any element, and icons below that element will use the specified size. This works sort of like font-size but for icons.

Next, I'll add styles for the default form control appearance.

input, button {
    --input-color: var(--control-color);
    --input-text-color: var(--control-text-color);

    border: 0.1em solid var(--input-color);
    border-radius: 0.2em;
    color: var(--input-text-color);
    outline: none;
    transition: border-color 0.3s, background-color 0.3s;
}

input {
    padding: 0.5em;
    background: transparent;
}

button {
    display: inline-flex;
    gap: 0.3em;
    align-items: center;
    justify-content: center;
    padding: 0.5em 1em;
    background: var(--input-color);
}

:is(input, button):is(:hover, :focus-visible) {
    --input-color: var(--control-highlight-color);
}

Although some people frown upon setting defaults directly using element selectors, my experience is that it works fine almost all of the time. There are cases where the defaults can get in the way, but then it's a matter of simply adding a :not(some-selector) to the element selectors or adding appropriate overrides. I think it's overall a good compromise.

It's a good idea to order your properties in a certain way. It doesn't matter that much which way you order them as long as you can keep a consistent order. The way I order them is basically like this:

  • Custom properties
  • The content property
  • Properties that affect the placement of the element within the different layout models (e.g., flex: 1)
  • Properties that affect the placement of the element in general (e.g., position: absolute)
  • Properties that affect the element's internal layout (e.g., display: flex or gap: 1em)
  • Other properties ordered from outside to inside (e.g., border-radius: 0.2em is outside, font-size: 120% is inside)
  • Effects (animation, transition, shadows, etc.)

After the form controls, I want to style the page sections that repeat. For example, the page header and the main section:

body > header {
    display: flex;
    align-items: center;
    gap: 1em;
    flex-wrap: wrap;
    background: var(--secondary-background-color);
}

body > header,
main {
    padding: 1em;
}

main {
    display: flex;
    flex-direction: column;
    gap: 1em;
}

The reason header is selected as body > header and not just header is that a <header> element can appear in several other places, not just as the page header. For example, we could have a section header where the header appears at the top of the <section> tag.

This ought to cover it for the general styles. I'm going to do the page-specific ones now.

Page styles

The styling of the page heading is quite simple: just center it:

h2 {
    padding: 0 1em;
    text-align: center;
}

I've also added a 1em padding left and right to match the page header padding. This will help keep the layout more consistent on very small screens (remember that high zoom ratio also counts as "small screen").

The form and the location lists are small UI bits. They are not supposed to take up the entire width of the page. So I want them to have a limited width and be centered on the page.

x-search,
x-location-list {
    display: block;
    width: 30em;
    max-width: calc(100% - 2em);
    margin: 0 auto;
}

The max-width is for screens narrower than 32em. This is me thinking in relative units. I don't care what 32em is going to be in device pixels or even CSS pixels. What I care about is that 30em is the width of the UI components (derived by precision eyeballing), and that it will shrink if it doesn't fit them plus the 1em margin on each side for a total of 32em.

Note another thing. Although I say "if it doesn't fit", you will notice that there's no if block in CSS. This idea is expressed declaratively using the max-width property. This means one less moving part – an if would be a moving part – and that's always a good thing. This is why you want to do as much as you can in CSS before you even think about JavaScript.

Next, I'll deal with the search form. I want the search field and the button to be presented as unit so that the button appears as if it's part of the field. But before I get to that part, I'll make sure the form uses a flex layout.

x-search form {
    display: flex;
    align-items: center;
    width: 100%;
}

x-search label {
    flex: 1;
    width: 100%;
}

Other than the standard flex stuff, you'll notice that I'm using the custom element as a namespace for the elements underneath it.

x-search input {
    width: 100%;
    border-radius: 0.2em 0 0 0.2em;
}

x-search button {
    flex: none;
    border-radius: 0 0.2em 0.2em 0;
}

Here I'm using the border-radius shorthand to... cut the corners... where these elements meet each other.

To handle small screens, I will force the form to assume a vertical layout (button below the input):

@media (width < 30em) {
    x-search form {
        flex-direction: column;
        align-items: stretch;
    }

    x-search input {
        border-radius: 0.2em 0.2em 0 0;
    }

    x-search button {
        border-radius: 0 0 0.2em 0.2em;
    }
}

The media query is using a 30em width as the breakpoint. This is based on the form content, not the screen size. In other words, it's specific to this particular form. This is why I have this style right underneath the styles for the form. Although there's sometimes a need to synchronize breakpoints for several UI elements, it's not always necessary. And it's most certainly not necessary (nor desirable) to base breakpoints on arbitrary screen sizes that won't even apply to your layout in many cases.

Instead of going for screen sizes, I'm looking for a point where the form is about to break, and that's my "break" point.

In order to figure out what the size is in em (or rem) I use the following calculation:

let baseFontSize = window.getComputedStyle(document.documentElement).getPropertyValue('font-size')
let fontSizeInPx = parseInt(baseFontSize, 10)
let remWidth = window.innerWidth / fontSizeInPx

I've written about a bookmarklet that will give you this value with a single click, so grab that if you haven't already. You're welcome.

Neat thing about using em for media queries is it works for your particular layout regardless of your default font size setting as the same em value is valid for any default font size as long as everything else on your page is based on em (or rem).

Last but not least, I need to deal with the result list. For this, I will first insert the contents of the templates into the list itself.

<x-previously-used-locations>
    <!-- .... -->
    <ul>
        <li>
            <button>
                <x-icon name="star"></x-icon>
                <span>Palmyre, Black River, Mauritius</span>
            </button>
            <button>
                <x-icon name="trash"></x-icon>
                <span class="alt-text">Delete</span>
            </button>
        </li>
    </ul>
    <!-- .... -->
</x-previously-used-locations>

<x-search-results>
    <!-- ... -->
    <ul>
        <li>
            <button>
                <x-icon name="pin"></x-icon>
                <span>Quatre Bornes, Quatre Bornes, Mauritius</span>
            </button>
        </li>
        <li>
            <button>
                <x-icon name="pin"></x-icon>
                <span>Black River, Black River, Mauritius</span>
            </button>
        </li>
    </ul>
    <!-- ... -->
</x-search-results>

The data inserted is arbitrary but still somewhat indicative of what I'll actually have in there. This is based on reading the docs of the geolocation API (to find out what it offers) as well as thinking a bit about what would be useful to have in there. There are countries that have towns and cities that have the same name, and the only way to differentiate between them is to also look at the region (this is especially true in the US), so I'm including the name of the locality as well as the region and country.

Now that I have these in place, I can add styling for them.

x-location-list,
x-location-list ul {
    display: flex;
    flex-direction: column;
    gap: 0.5em;
}

x-location-list li {
    display: flex;
    align-items: center;
    gap: 0.5em;
}

x-location-list button:first-child {
    text-align: left;
    width: 100%;
}

x-location-list button:first-child:not(:hover):not(:focus-visible) {
    --input-color: transparent;
}

Most of it is just the standard flexboxing. Probably the only interesting bit is the one at the bottom. This is an override for the style assigned to the buttons. I'm overriding the --input-color property to make the list items have no background and border until hovered or focused.

One last bit I want to check is what these look like when I mark either the <x-previously-used-locations> or <x-search-results> as hidden using the hidden attribute. Sometimes the styling interferes with my ability to hide them by setting this attribute, so I check that and make sure nothing is interfering.

After I'm done with the lists, I can remove the fake hard-coded list items.

Conclusion

i hope this was at least somewhat informative for you. in the next part, I'll be going through creating and adding the SVG spritesheets to the project.

Posted in Programming tips
Back to top