Code Formatting and Style
Last updated:
How HTML, CSS, JavaScript, and TypeScript should look in any codebase — indentation, line breaks, and team-wide conventions.
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/barunless 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— usepxonly when the design genuinely needs it -
shared styles first;
@mediablocks 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
.fooare 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.
transitionlists
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,autocompletewhen 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
functionfor top-level helpers when it reads clearer -
querySelector/querySelectorAllwith 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.