01 — Foundation

Readable code is part of the product

Consistent formatting reduces review noise and makes every codebase easier to work in.

These standards describe how HTML, CSS, JavaScript, and TypeScript should look in your editor, in review, and in production — on any stack, any framework, any repository. The goal is not aesthetic pedantry; it is code another developer can scan, diff, and change without reformatting mentally on every file.

Agree the rules with your team, then enforce them with a formatter in CI where you can (Prettier, Biome, or equivalent) plus code review for what automation misses. The samples on this page illustrate the style; they are not tied to a single project or toolchain.

Related: HTML, CSS, JavaScript, and CSS architecture.

02 — Shared

Rules for every language

What applies before you pick HTML, CSS, or JavaScript specifics.

  • indent with 4 spaces — no tabs
  • comments only when the code needs them — most files and snippets should not
  • line length: readable, no hard cap — but avoid long unreadable lines
  • realistic names — match your project’s conventions, not foo / bar unless teaching generics

Enforcement

Application code — run your formatter in CI; fail the build or block merge when it drifts. Fix locally before push.

Docs, snippets, and gists — apply the same rules by hand when automation does not reach embedded code blocks.

Anti-patterns in teaching material — only break these rules when demonstrating what not to do; label bad examples clearly.

Contributing to Frontend Foundations — this repo uses Prettier (pnpm format, 4 spaces, single quotes). Pattern and standards pages should follow this page even where strings are not auto-formatted yet.

03 — CSS

CSS formatting

One declaration per line, global-first layout, breakpoint deltas only.

  • one selector or rule per line — no cramming multiple declarations on one line
  • one declaration per line — shorthand only when very common; prefer longhand for clarity (build tools can compress later)
  • prefer rem — use px only when the design genuinely needs it
  • shared styles first; @media blocks after, with only what changes — see responsive strategy and LSCSS architecture
  • named custom media — --mobile, --from-tablet, --desktop — not raw pixel widths in every file
  • empty blocks like .foo are fine in docs that show structure — do not ship empty rules in production CSS
  • group selectors that share the same rules on one line — .button, .pagefind-ui__button
  • nest children, states, and breakpoints inside the component block when it keeps the partial readable — &:hover, .icon, @media (--mobile)
  • multiline values when several related properties belong together — e.g. transition lists

Some teams prefer flat .parent > .child selectors; others nest with & inside component blocks. Both are fine — pick one approach per codebase and stay consistent. Your build pipeline must support the syntax you choose.

Example — button component (excerpt)

Production partials can be long. The excerpt shows grouping, nesting, custom media, and state — the rest of the file follows the same rules.

.button {
    border: 0;
    font-family: var(--ff-display);
    font-weight: var(--fw-heavy);
    transition:
        var(--transition-s) color ease,
        var(--transition-s) border-color ease,
        var(--transition-s) background-color ease;
    text-decoration: none;
    cursor: pointer;
    color: var(--c-white);

    @media (--desktop) {
        font-size: var(--fs);
    }

    @media (--tablet) {
        font-size: var(--fs-s);
    }

    @media (--mobile) {
        font-size: var(--fs);
    }
}

.button:not(.button--close, .button--toggle) {
    display: inline-flex;
    align-items: center;
    gap: 0 var(--space-s);
    padding: var(--space-s) var(--space);
    clip-path: var(--clip-button);

    &:hover,
    &:focus {
        .icon:last-of-type {
            translate: 6px 0;
        }
    }
}

Avoid

.site-header > .inner > .header-bar { display:flex;align-items:center; }
@media (max-width:767px){.site-header .primary-nav{display:none;}}

04 — HTML

HTML formatting

Complete, accessible markup — formatted for humans reading the source.

  • 4-space indent — nesting depth follows the document, not a fixed limit
  • do not omit attributes the UI needs — labels, for, id, name, ARIA, autocomplete when relevant
  • do not strip accessibility attributes from snippets to save space — incomplete markup teaches incomplete habits
  • void elements follow HTML5 — no XML-style self-closing slash on input, img, br, etc.
  • few attributes: keep on one line; many or long values: one attribute per line
  • blank line between sibling block elements — h2, p, ul — so structure scans quickly

Example — content block

<article class="content">
    <h2>Lorem ipsum dolor</h2>

    <p>Sit amet consectetur adipiscing elit sed do eiusmod tempor.</p>

    <ul>
        <li>
            <strong>Incididunt ut labore.</strong> Ut enim ad minim veniam quis nostrud exercitation.
        </li>

        <li>
            <strong>Duis aute irure.</strong> Dolor in reprehenderit in voluptate velit esse cillum.
        </li>
    </ul>
</article>

Example — form control

<label for="email">Email address</label>

<input
    id="email"
    name="email"
    type="email"
    autocomplete="email"
    aria-describedby="email-hint email-error"
    aria-invalid="true">

Avoid

<input placeholder="Email" type="email"/>
<label>Email</label>

05 — JavaScript

JavaScript and TypeScript formatting

Semicolons, single quotes, modules, guarded DOM access, and conditional loading.

  • always use semicolons where the grammar expects them
  • single quotes for strings — double quotes when the string contains a single quote
  • ES modules — import / export; no globals for feature code
  • TypeScript: type class fields and parameters; early return in constructors when required nodes are missing
  • arrow functions for listeners and short callbacks — named function for top-level helpers when it reads clearer
  • querySelector / querySelectorAll with guards — only load and init when matching DOM exists
  • dynamic import() for optional features — entry file branches on presence, not one bundle for every page
  • multi-line template literals are fine when they improve clarity

Canonical example — class module

export default class AlertBlock {
    private alertElem!: HTMLElement;
    private closeButton!: HTMLElement;
    private activeClass!: string;

    constructor(
        alertElem: HTMLElement | null,
        closeButton: HTMLElement | null,
        activeClass = 'is_active',
    ) {
        if (!alertElem || !closeButton) {
            return;
        }

        this.alertElem = alertElem;
        this.closeButton = closeButton;
        this.activeClass = activeClass;

        this.init();
    }

    private init(): void {
        this._attachEventListener();
    }

    private _attachEventListener(): void {
        this.closeButton.addEventListener('click', (e: MouseEvent) => {
            e.preventDefault();
            this._closeAlert();
        });
    }

    private _closeAlert(): void {
        this.alertElem.classList.remove(this.activeClass);
        this.alertElem.setAttribute('aria-hidden', 'true');
    }

    openAlert(): void {
        this.alertElem.classList.add(this.activeClass);
        this.alertElem.setAttribute('aria-hidden', 'false');
    }
}

Example — entry module

Query once, load features only when the DOM needs them. A full entry file repeats the same pattern for other optional features.

import { loadModule } from './utilities.js';

const alertBlocks = document.querySelectorAll<HTMLElement>('.card--alert');

if (alertBlocks.length) {
    loadModule(
        () => import('./AlertBlock.js'),
        'AlertBlock',
        (module) => {
            const AlertBlock = module.default;

            alertBlocks.forEach((alert) => {
                const closeButton = alert.querySelector<HTMLElement>('.button--close');

                if (closeButton) {
                    new AlertBlock(alert, closeButton);
                }
            });
        },
    );
}

Avoid

var btn=document.querySelector(".menu")
btn.onclick=function(){document.querySelector(".site-header").classList.toggle("is_open")}

06 — Review

Before you approve

A quick formatting pass in code review — application code, docs, and snippets.

  • changed files pass the team formatter — or match this page if you have no formatter yet
  • CSS uses named breakpoints and delta-only media blocks — not one-off magic numbers copied between files
  • HTML keeps required labels, associations, and ARIA — not minimal markup for screenshot aesthetics
  • JavaScript and TypeScript guard DOM access and load optional code only when needed
  • deliberately bad samples are labelled — not accidental sloppy formatting

Pick a house style, document it, and enforce it. Debates in review should reference agreed rules — not individual preference each PR.