Hajime, the duck guy

Monday, January 6, 2025, 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

Webfonts, asset preloading, responsive grid layouts

In the previous part, I've finished the basic content structure for the forecast page. In this part, I'll do the presentation – the CSS.

Now this page is a bit more complex than the place search one. Therefore, I will be doing a lot of "messing around", which is basically tweaking of the CSS to achieve some effect. I'm not going to describe the messing around for every little thing, because that's going to require me to write a book – and a very boring one at that. Instead, I'm simply going to present you with the final result. I just want you to know that a bit of trial and error goes into that.

Changing the font

In the past iterations, I haven't dealt with webfonts. That's mostly because I wasn't intending to, but then would the stylesheet be complete without a custom font? Of course it would, but it doesn't sound so dramatic if I said so, does it?

After a bit of looking around, I found a rounded sans-serif font called Comfortaa. I particularly like the modern geometric look of the font, and I think the rounded stokes will go nicely with the icon theme. I also believe in synchronicity. Using this font sounds almost intentional – the reason we check the weather is we want to feel comfortable, after all. 😅

When I deal with fonts, I never link to the copies on the Google Fonts servers. Doing this can throw you into compliance issues even if you don't really care about user's privacy and all that, so I think it's a good idea to serve them from your own servers/CDNs. This might have changed when you're reading this, but as of right now, using fonts via the Google Fonts servers isn't a GDPR-compliant way of doing it. It counts as a 3rd party processing personally identifiable information without consent.

What I do instead is download the font, convert to a webfont myself, and host the font on the same server as the rest of the assets. For conversion, I use Transfonter.

To download the font, I use the "Get font" button to access the download page. From the download page, I can either click "Download all", or the "Download" icon next to the font. In the downloaded zip file, there's a static folder, which contains the goodies.

The static folder contains several files, one for each weight. I'm only going to use the 'regular' weight in this case. This means I lose the option to bold the text for emphasis, but that's fine. I can still emphasize text using capitalization, size, color, etc. I'm not going for the 'light' weight, because it's too thin (in my opinion) and won't render well at small sizes, especially on non-MacOS computers at smaller sizes (so yes, that means you should not test pages only on MacOS and think what you see is relevant to the majority of your users).

I upload the single file to Transfonter, and generate the webfont bundle with the default options. The bundle it generates comes with two font files, in .woff and .woff2 formats, and a stylesheet. The font files go into the same folder as all other files (remember, I'm using a completely flat hierarchy), and I copy thee contents of the stylesheet to the top of the common.css:

@font-face {
    font-family: 'Comfortaa';
    src: url('Comfortaa-Regular.woff2') format('woff2'),
    url('Comfortaa-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

This block is placed at the top of the CSS so that the font file URLs are discovered as early as possible. Additionally, I'm also going to add preload.

<link rel="preload" href="Comfortaa-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">

I add this as the first <link> tag to page. This goes in both the index.html and place.html.

Why is there only one preload link if we have two font files? Well, keep in mind that browsers only need one of them, never both. Whether they pick woff or woff2 depends on whether they support the newer version or not. The preload improves the performance for the newer browsers (which is the majority of them right now). For the older browser... the preload is ignored. That's fantastic if you think about it – it won't cause the older browsers to download both files.

With this in place, the font now loads in parallel with the stylesheet. It will still not load before the stylesheet, so a standard sans-serif font still show initially. Frankly, the difference is pretty much negligible regardless of what the network tab is telling me, because all the code is so tiny. You could say it's a premature optimization. However... it's a premature optimization that cost me only one line and has no negative side-effects. I'll take it.

Adding the common stylesheet

I'm adding the common stylesheet to the page.

<html lang="en">
    <head>
        <!-- .... -->
        <link rel="stylesheet" media="screen" href="common.css">
        <!-- .... -->
    </head>
    <!-- .... -->
</html>

Reviewing the content structure

Since I wasn't quite sure about the content structure last time, I ran into a few cases this time that didn't quite fit the presentation I was going for.

Last time I had decided to keep the visible labels for all weather information in the current weather section. This is ideal from the accessibility standpoint, but it will probably not fit on a smaller screen. Therefore, I'm adding new icons to that section. While I'm at it, I'm also going to insert hard-coded values for each piece of information (e.g., 2°C for temperature).

<dl>
    <div class="current-temperature">
        <dt class="alt-text">Current weather and temperature</dt>
        <dd>
            <x-icon name="sunny"></x-icon>
            <span>2°C</span>
        </dd>
    </div>
    <div class="min-temperature">
        <dt class="alt-text">Minimum temperature</dt>
        <dd>
            <x-icon name="temp-min"></x-icon>
            <span>-2°C</span>
        </dd>
    </div>
    <div class="max-temperature">
        <dt class="alt-text">Maximum temperature</dt>
        <dd>
            <x-icon name="temp-max"></x-icon>
            <span>-2°C</span>
        </dd>
    </div>
    <div class="precipitation">
        <dt class="alt-text">Precipitation probability</dt>
        <dd>
            <x-icon name="precipitation"></x-icon>
            12%
        </dd>
    </div>
    <div class="pressure">
        <dt class="alt-text">Atmospheric pressure</dt>
        <dd>
            <x-icon name="pressure"></x-icon>
            987hPa
        </dd>
    </div>
    <div class="humidity">
        <dt class="alt-text">Humidity</dt>
        <dd>
            <x-icon name="humidity"></x-icon>
            43%
        </dd>
    </div>
    <div class="wind">
        <dt class="alt-text">Wind</dt>
        <dd>
            <x-icon name="wind"></x-icon>
            <span class="wind-speed">5km/h</span>
            <x-icon class="wind-direction" name="wind-direction"></x-icon>
            <span class="wind-direction-label">NW</span>
        </dd>
    </div>
</dl>

With these changes, I can move on to styling.

Adding new icons

As before, I search the project for all usages of the <x-icon> element, and add a bunch of new icons. Additionally, I'm adding the weather icons.

Screenshot of the updated SVG spritesheet with new icons

I'm not entirely counting on the icons to describe exactly what they are about. I find that it's generally very difficult to do this save for fairly common and standardized icons such as edit, info, location, close (x). Therefore, I count on the icon to work within a context, in conjunction with other information on the page (e.g., hPa unit that suggest atmospheric pressure), rather than in isolation.

I also count on them to be learnable symbol that will eventually replace the meaning user assigned to it (e.g., help the user orient themselves when layout changes). For these icons, it's therefore more important that they are distinct.

Reset adjustment

Since I haven't used <dl>, <dt> and <dd> before, I need to add them to the global reset in common.css.

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

I will also need to reset the table header cells:

th {
    font: inherit;
}

For now this is all the additional reset I need.

The header

One thing I need to adjust as far as the header goes is the horizontal layout. I need to make sure the logo and the place switch controls are spread out:

body > header {
    /* .... */
    justify-content: space-between;
    /* .... */
}

In the index.css file, I'm declaring the layout of the current place widget:

x-current-place {
    display: flex;
    align-items: center;
    gap: 0.5em;
}

Nothing special. Just a simple flex layout.

The control to change the selected place is just a link. I am styling it to look like a button, but only when focused or hovered. Since the button style is defined in the common.css, I'm going to add the selector for this link there.

input, button, x-current-place a {
    /* .... */
}

Additionally, in the index.css, I add styles specific to this use case:

x-current-place a {
    --input-color: transparent;
    --input-text-color: inherit;

    padding: 0.5em;
}

x-current-place a:is(:hover, :focus-visible) {
    --input-color: var(--control-color);
    --input-text-color: var(--control-text-color);
}

The selector that's specific to index page ended up in common.css. This is currently the only real gripe I have with CSS. This definitely makes the code harder to maintain. Copying the CSS would make it even worse, though, so I didn't want to do that.

I could also introduce a build step just for this, but... I don't want to go there.

Weather summary layout

I'm going to work out the weather summary. Unlike your typical day job, I'm designing the UI as I go, so I need to first figure out the individual bits before I figure out how they will fit together. If I have a mockup, or a napkin sketch, I can do it the other way around, but this time I'm working off a vague idea in my mind.

I know I wan the individual pieces of information to be in tiles. I also know I want the weather condition and current temperature to be a big tile.

After thinking about this for a while, I'm settling on the grid and media queries. The initial layout looks likes this:

Screenshot of the summary on a larger display
The large-screen version of the layout stretches horizontally across the page

First I define the grid layout. I have 7 tiles in total. One large one, and six small ones. The big one goes on the left, and then I have a neat 3×2 matrix.

x-weather-summary dl {
    --columns: 5;
    --rows: 2;
    --template-areas:
            "m0 m0 w1 w2 w3"
            "m0 m0 w4 w5 w6";
    --tile-border: 0.2em;

    display: grid;
    grid-template-columns: repeat(var(--columns), 1fr);
    grid-template-areas: var(--template-areas);
    gap: var(--tile-border);
    padding: var(--tile-border);
    aspect-ratio: var(--columns)/var(--rows);

    background: var(--secondary-background-color);
}

CSS custom properties are great. I can separate the things I care about from thee things I care less about, and later I'll be able to override only those parts of the declaration block by overriding only the CSS custom properties. (You'll see it later). In this case, I know I'm going to be manipulating the grid layout, and for that I need the column count, row count, and the template area labels. The last one, --title-border is for readability, so I know what the number means: I'm using the background colors to draw a fake border.

I don't give the rows any explicit height. These are defined later by giving each child an aspect ratio.

The template areas are named using 2 characters. You will frequently see people use a b c, x y z, and similar one-letter names. I find it's virtually impossible to reliably rename those, so I opt for two-character names. The reason I use letter-number combination is that it's less likely to also appear somewhere else in the code compared to arbitrary two-letter combinations. In this particular case, m0 stands for 'main area' (temperature and weather condition), and w* are the other weather info tiles, 1 through 6. At any rate, I always make sure they are all the same length, so I can visualize the layout – it's easier to do when the characters align.

Every tile is wrapped in its own <div>, so I'm targeting those, and giving each tile a flex layout to center the contents, and I'm also giving them an aspect-ratio of 1 so they're always square regardless of the layout.

x-weather-summary dl > div {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1;

    background: var(--background-color);
}

Finally I assign each of the elements to the grid areas. This will give me more flexibility in defining the layouts. The trade-off is that I will need to redo the layout every time I add or remove any detail. For what I had in mind in terms of the presentation, it was a necessary trade-off as you will see shortly.

x-weather-summary .current-temperature {
    grid-area: m0;
}

x-weather-summary .min-temperature {
    grid-area: w1;
}

x-weather-summary .max-temperature {
    grid-area: w2;
}

x-weather-summary .precipitation {
    grid-area: w3;
}

x-weather-summary .pressure {
    grid-area: w4;
}

x-weather-summary .humidity {
    grid-area: w5;
}

x-weather-summary .wind {
    grid-area: w6;
}

Tile detailing

The big tile should have larger text size:

x-weather-summary .current-temperature {
    font-size: 300%;
}

I also want a vertical layout for the wind section so that the wind direction appears below the wind speed.

x-weather-summary .wind dd {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 0.5em;
}

Finally, for all the tiles except the large one, I want the icon that represents the type of weather information to appear in the top left corner:

x-weather-summary x-icon:not(.current-weather-condition):not(.wind-direction-graphic) {
    position: absolute;
    top: 0.2em;
    right: 0.2em;
}

Rather than give each <x-icon> element that represents the information type of the tile a separate class, I decided to just select all icons and then exclude the ones I don't want using the :not() pseudo-class.

Layout variations and responsiveness

Now I can deal with other layouts. To get my breakpoints, I reduce the window width until I reach a point where the layout looks a big squished. Then I back up a hair, and run my breakpoint bookmarklet to get the breakpoint width in em.

The first breakpoint – point where something breaks – is at 50em. I'm switching to a layout where I have 4 tiles on the right, and 3 on the left.

@media (width < 50em) {
    x-weather-summary dl {
        --columns: 10;
        --rows: 6;
        --template-areas:
                "m0 m0 m0 m0 w3 w3 w3 w4 w4 w4"
                "m0 m0 m0 m0 w3 w3 w3 w4 w4 w4"
                "m0 m0 m0 m0 w3 w3 w3 w4 w4 w4"
                "m0 m0 m0 m0 w5 w5 w5 w6 w6 w6"
                "w1 w1 w2 w2 w5 w5 w5 w6 w6 w6"
                "w1 w1 w2 w2 w5 w5 w5 w6 w6 w6";
    }
}

The reason I have 6 rows for that is elementary mathematics: 6 is the smallest number that is divisible by both 2 and 3 – I have 3 rows in the left group (2 for the big tile, 1 for the small) and 2 rows on the right (2×2 matrix). The 10 columns is derived the same way. I'm joking. I mean, it's all true, but that's not how I arrived at those numbers. I'm not that kind of person. I actually sketched the layout on a square grid notebook to figure out what I need.

Drawing of the weather app layout
I sketched the layout and then counted the columns and rows

The final results looks about the same:

Screenshot of summary layout at 50em
At 50em we get this layout

I'm going to quickly reiterate why I'm using em as the media query. For some reason, some people think this is a bad idea. I assure you that's just paranoia, and has nothing to do with reality. em media queries work just fine. Before you react to what I've just said, I will direct your attention to two things that I never do in this post:

  1. I never attempt to recalculate that into pixels or vice versa
  2. I never talk about physical screen sizes

That's the trick. You need to forget pixels and screen sizes even exist. They don't actually matter at all. I know it may be hard to believe, but you're literally seeing it happen throughout this series. You've never seen me talk about pixels (except maybe in the context of SVG sprites and here in this section).

The downside is that you do need something like that bookmarklet to get the viewport width in em (or rem – it doesn't matter for media queries which you use), because people making the browser dev tools don't seem to agree with me. Tbh, I think it's not that big of a loss.

The advantage is that now I don't have to worry about whether the layout will continue to work if the user changes the default font size, zooms in, or anything else that affects the layout. The breakpoint itself is now fully responsive and will adjust itself accordingly. Magic!

Ok, back to layouts. Moving further down to 36em, I get more breakage, so I adjust the layout as follows:

@media (width < 36em) {
    x-weather-summary dl {
        --columns: 4;
        --rows: 4;
        --template-areas:
                "m0 m0 m0 w3"
                "m0 m0 m0 w4"
                "m0 m0 m0 w5"
                "xx w1 w2 w6";
    }
}

Or in the form of a picture:

Screenshot of summary layout at 36em
At 36em we have one blank spot in the grid, but I still like it

And last and the least, at only 26em I adjust the layout two a two-column grid:

@media (width < 26em) {
    x-weather-summary dl {
        --columns: 2;
        --rows: 5;
        --template-areas:
                "m0 m0"
                "m0 m0"
                "w1 w2"
                "w3 w4"
                "w5 w6";

    }
}

Which translates to:

Screenshot of summary layout at 26em
At 26em we get a simple 2-column layout with the big tile at the top

I could technically go even lower, but there isn't much point in doing that. It will not be worth it.

Overall page layout

I'm skipping the forecast section and going for the page layout. The reason for this is that the forecast section is a table, and there isn't much information that would help for the page layout that I would gain from detailing it. My goal is to eliminate unknowns as quickly as possible, so I'm not going to dwell on parts that I already (think I) know what to do with.

My initial thought was that I'd just do the max-width on the <main> element and be done with it, but decided to do the less lazy version instead. Where the space permits, I'm going to reuse the 36em breakpoint layout for the summary (the square one with one blank tile), and lay the forecast out side-by-side with the summary.

This time I'm going in the opposite direction, expanding the viewport slowly until the layout starts to feel a bit too sparse. Around 80em it starts to get a bit too roomy.

I'm modifying the layout for the summary so that the square layout is used for both <36em and >80em cases:

@media (width < 36em), (width > 80em) {
    x-weather-summary dl {
        --columns: 4;
        --rows: 4;
        --template-areas:
                "m0 m0 m0 w3"
                "m0 m0 m0 w4"
                "m0 m0 m0 w5"
                "xx w1 w2 w6";
    }
}

Multiple media queries can be specified using a comma (,), which is perfect for this case. In this case, it means <36em or >80em, whichever applies.

The <main> element is the common container for the two sections, so I'm going to apply the following styles to it:

main {
    display: grid;
    grid-template-columns: 1fr;
}

@media (width > 80em) {
    main {
        grid-template-columns: 34em 1fr;
    }
}

With these changes, I get this past the 80em screen width:

Screenshot of the weather app at 80em+ screen
width
It may not look like it right now, but trust me, it's a two-column layout

Conclusion

In this part, I've completed some of the styling for the weather information page. I've covered fonts, preloading, CSS grids, and responsive layouts.

I still need to finish off the 10-day forecast section, but I'll leave that for the next part as this is already getting a bit too long.

Posted in Programming tips
Back to top