Against Horizontal Scroll (Even More)

Over on lobste.rs I saw that Alex (aka matklad) posted his great post, Against Horizontal Scroll. I just fought this on my site rewrite so here are some tips I can add.

Viewport Units (e.g. vw)

Be careful when using vh/vw, including their svh/svw counterparts1. These units do not account for scrollbars, so a simple body { width: 100vw } will result in a body that is wider than the viewport as soon as your content is tall enough to have a vertical scrollbar.

Speaking of scrollbars: test on devices with scrollbars turned on or you might miss horizontal scrolling caused by the existence of a vertical scrollbar. And you might miss nested vertical scrollbars. If you develop on a MacBook with a trackpad, for example, you do not have scrollbars enabled:

Screenshot of macOS System Preferences showing how to enable scrollbars

If you use body { min-height: 100vh; }, e.g. if you have a footer and always want it pushed to the bottom of your grid layout, you need to account for margin-collapse between the body and its first child or last child. If the first child has top margin or the last child has bottom margin, your body will be >100vh. You can either:

  • make sure the children don’t have top/bottom margin
  • add body { padding-block: 0.05px; }, since padding stops this margin-collapse
  • make your body a flex or grid container, which also stops margin collapse

Box sizing

Wherever you use padding/border either account for it in your width calculations or use box-sizing: border-box to let css figure it out for you. This doesn’t handle margin though. More details over at CSS-Tricks, of course.

Unconstrained Markdown Content

If you generate your content from markdown and aren’t always setting widths on everything as a result, make sure you handle that. I would:

  • Set a max width on media, either globally or constrained to your markdown-generated content:

    global.css
    img,
    svg,
    video {
    max-width: 100%;
    height: auto;
    }
  • Wrap <table>s in a wrapping div, and constrain the wrapping div. It’s trivial to do this with a Rehype plugin, e.g. on this site I configured Astro to use rehype-wrap-all:

    astro.config.ts
    import rehypeWrap from "rehype-wrap-all";
    export default defineConfig({
    // ...
    markdown: {
    // ...
    rehypePlugins: [
    // ...
    [
    rehypeWrap,
    {
    selector: "table",
    wrapper: "div.markdown-table-wrapper",
    },
    ],
    ],
    },
    });

    And I constrained .markdown-table-wrapper like so:

    global.css
    .markdown-table-wrapper {
    max-width: 100%;
    overflow-x: auto;
    }

    Which looks like this: Video showing a horizontally-scrolling table that is too wide to fit on the page

Grid

Grid layouts can be super simple ways of skipping a lot of math. For example, you can make a 2-column grid (grid-template-columns: 1fr 1fr) and then give it any gap you want, and the columns will just fill in the remaining space. If you used block items with margins, you (might) have to keep track of the margin when figuring out your max width. Using grid will make it easier to stay in bounds by default.

End-to-End Testing

Lastly, if you’re really silly, you can add Playwright testing into the mix. Just add a bunch of devices into the test matrix2, visit various places in your site, and, using a custom matcher, assert the page doesn’t have a horizontal scrollbar, e.g.:

article.spec.ts
import { test, expect } from "../fixtures/fixtures.js";
test.describe("homepage", () => {
test("has no scrollbars", async ({ page }) => {
await page.goto("/");
await expect(page.locator("html")).not.toHaveHorizontalScrollbar();
});
});

EDIT: While writing this article I tweaked a style that I thought was totally harmless and it caused horizontal scroll on exactly one page on my whole site, and my tests caught it. Maybe testing this isn’t so silly.

Footnotes

  1. More details at The Large, Small, and Dynamic Viewports

  2. The narrowest devices in Playwright’s device catalog are 320px wide, so use either “iPhone SE” or “Galaxy S9+” in your config. Ideally you’d have a device for each of the upper and lower bounds of all your media queries but I just yolo’d it and you can copy me.


<-Find more writing back at https://alan.norbauer.com