HTML Boilerplate

snippet

I probably create dozens of HTML documents each month, usually for experiments or minimal test cases. Consequently, I’ve long maintained a carefully crafted HTML template, included below, which I typically reduce to only retain pieces that are strictly necessary (e.g. removing the external style sheet or .stack utility).

Occasionally I refer to A Minimal HTML5 Document for the bare essentials (e.g. when constructing data URIs).

My shell history always includes the following command for easy access:

$ cp /path/to/snippets/boilerplate.html /tmp/dev/testcase.html #testcase

envsubst might be used for basic templating:

…
<title>${TITLE}</title>
…
<h1>${TITLE}</h1>
<main>${CONTENT}</main>
…
template.html
$ TITLE="hello world" CONTENT="lorem ipsum" envsubst < template.html
…
<title>hello world</title>
…
<h1>hello world</h1>
<main>lorem ipsum</main>
…

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Test Case</title>
    <meta name="viewport" content="width=device-width">
    <link rel="stylesheet" href="./styles/main.css">
    <style>
*,
*::before,
*::after {
    box-sizing: border-box;
}

:root {
    --h-size: 60ch;

    /* modular scale-ish; cf. https://www.modularscale.com/?1.5 */
    --spacing: 0.5rem;
    --spacing-sm1: calc(var(--spacing) * 0.667);
    --spacing-sm2: calc(var(--spacing) * 0.444);
    --spacing-sm3: calc(var(--spacing) * 0.296);
    --spacing-lg1: calc(var(--spacing) * 1.5);
    --spacing-lg2: calc(var(--spacing) * 2.25);
    --spacing-lg3: calc(var(--spacing) * 3.375);

    --animation-duration: 200ms;
}

body {
    max-width: calc(var(--h-size) + 2 * var(--spacing));
    margin: 1rem auto;
    padding-inline: var(--spacing);

    font-family: system-ui, sans-serif;
    line-height: 1.5;
    /* prevent font-size inflation; cf.
       https://kilianvalkhof.com/2022/css-html/your-css-reset-needs-text-size-adjust-probably/ */
    -moz-text-size-adjust: none;
    -webkit-text-size-adjust: none;
    text-size-adjust: none;
    /* disable macOS's default subpixel antialiasing for consistency across
       platforms; cf. https://dbushell.com/2024/11/05/webkit-font-smoothing/ */
    -webkit-font-smoothing: antialiased;
}

a {
    text-underline-offset: 0.15lh; /* XXX: magic value */

    @media (prefers-reduced-motion: no-preference) {
        transition-property: text-underline-offset, text-decoration-color;
        transition-duration: var(--animation-duration);
    }

    &:not(:focus, :hover, :active) {
        text-underline-offset: 0.1lh; /* XXX: magic value */
        /* XXX: a11y might be impacted by insufficient color contrast */
        text-decoration-color: color-mix(in srgb, currentColor, transparent 50%);
    }
}

img {
    display: block;
    max-width: 100%;
    height: auto;
}

input,
textarea,
select,
button {
    font-family: inherit;
    font-size: inherit;
}

textarea:not([rows]) {
    min-height: 8lh;
}

button,
summary {
    cursor: pointer;
}

.nonvisual:not(:focus, :active) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    white-space: nowrap;
    clip-path: inset(50%);
}

.stack > * {
    margin-block: 0;

    & + * {
        margin-block-start: var(--spacing);
    }
}
    </style>
</head>

<body>
    <header role="banner">
        …
    </header>

    <main>
        <h1>Test Case</h1>
        <p>lorem ipsum dolor sit amet</p>
    </main>

    <footer role="contentinfo">
        …
    </footer>

    <script type="module">
class MyElement extends HTMLElement {
    static tag = "my-element";
    static {
        customElements.define(this.tag, this);
    }
}
    </script>
</body>

</html>
boilerplate.html