12k
All articles

What Goes in a Modern HTML Boilerplate

Build a modern HTML boilerplate with the five required lines, useful metadata, favicon and social tags. See what to keep, add or remove from old templates.

OpenReplay Team
OpenReplay Team
What Goes in a Modern HTML Boilerplate

A modern HTML boilerplate needs five things: the HTML5 doctype, a lang attribute on <html>, a UTF-8 charset declaration, a viewport meta tag and a unique <title>. Everything else depends on whether the page will be shared, installed or bookmarked.

Most pages start from Emmet’s ! snippet, a framework’s index.html or an HTML5 Boilerplate copy that is ten years old. Nobody goes back and checks those lines, so outdated ones get copied from project to project.

This article goes through the document line by line. For each line it covers what the line does, what breaks without it, and whether it is required, optional or safe to delete. For head ordering and the wider metadata picture, read the companion piece on what actually belongs in the head of your document.

This is what Emmet’s ! produces:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

</body>
</html>

It’s a valid starting point. It has no description, canonical, social tags, icons or color-scheme, though, and its viewport includes a value you no longer need.

Key Takeaways

  • The required lines in a modern HTML boilerplate are the doctype, lang, <meta charset="UTF-8">, the viewport tag and <title>. Without any one of them, the page renders incorrectly, becomes less accessible or is invalid.
  • The lang attribute controls screen reader pronunciation, CSS hyphenation and browser translation. Google does not use it to detect a page’s language.
  • maximum-scale, minimum-scale and user-scalable=no stop low-vision users from zooming. Delete them wherever they appear.
  • A complete favicon set is favicon.ico, favicon.svg, a 180×180 apple-touch-icon.png and a web manifest with 192 and 512 pixel icons plus a maskable one.
  • X-UA-Compatible, shrink-to-fit=no and type="text/javascript" do nothing useful in a new project and can be removed.

Doctype and the lang Attribute

Required. <!DOCTYPE html> switches the browser into standards mode. Without it, the HTML parser falls back to quirks mode, which copies old browser layout bugs. You then get box-model and table sizing differences that no amount of CSS debugging explains.

Required. The lang attribute on <html> does three jobs:

  • It tells screen readers which pronunciation rules to apply.
  • It lets CSS hyphens: auto break words using the correct dictionary.
  • It helps browser translation features work out the page’s language.

Without it, a screen reader may read French text with English phonetics. The value is a BCP 47 language tag such as en, de or pt-BR, and child elements can override it, as MDN’s lang reference describes.

lang handles accessibility and language, not search ranking. Google Search states that it ignores lang and detects the language from the page content.

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

Charset First, Then Viewport

Required. <meta charset="UTF-8"> has to fall within the first 1024 bytes of the document. That’s why it goes first in <head>, ahead of <title> and any other text the browser might otherwise decode with the wrong encoding. If it’s missing or comes too late, accented characters and emoji in your title turn into mojibake.

Required. The viewport tag tells mobile browsers to lay the page out at the device’s width. Without it, phones render a desktop-width layout (usually around 980px) and shrink it down, so your media queries never fire. width=device-width is enough on its own, and initial-scale=1 is usually unnecessary. See MDN’s viewport reference for the full list of values.

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">

Optional, test first. <meta name="text-scale" content="scale"> is defined in the CSS Fonts Level 5 draft, and only Chromium-based browsers ship it. It makes the root font size follow the text size the user picks in their OS or browser settings. As a result, only rem and em text grows, and fixed-height components can overflow. Increase the system text size and check your layouts before you ship it.

Title, Description and Canonical

Required. <title> names the page in browser tabs, bookmarks, history and search results. If you leave the default Document, every tab you open looks identical.

Optional but recommended. <meta name="description"> offers a summary for search snippets. Google may rewrite it if it finds page content that fits the query better, and it truncates snippets based on display width. That makes character-count rules unreliable.

Optional but recommended. <link rel="canonical"> names the preferred URL when the same content is reachable at more than one address, for example with query strings or trailing-slash variants. For more detail, see SEO-related HTML tags and how to optimize them.

<title>Page title – Site name</title>
<meta name="description" content="One or two sentences describing this page.">
<link rel="canonical" href="https://example.com/page">

Open Graph and Twitter Card Tags

Optional, required for anything people share. Open Graph tags control the link preview on social platforms and in chat apps. Without them, a shared link shows up as a bare URL or as a card built from whatever the scraper finds.

The Open Graph protocol lists og:title, og:type, og:image and og:url as required properties, and it says a page with no type should be treated as website. Set og:url to the same value as your canonical URL, so links that carry tracking parameters don’t get cached as separate pages. For og:image, Meta recommends an image of at least 1200×630 pixels, kept close to a 1.91:1 ratio.

X reads its own twitter:* tags, written with name rather than property, and falls back to the matching og:* values when they’re missing. In practice, the only X-specific tag you need is twitter:card, and only if you want the large-image card.

<meta property="og:title" content="Page title">
<meta property="og:description" content="Description for link previews.">
<meta property="og:image" content="https://example.com/og-image.jpg">
<meta property="og:url" content="https://example.com/page">
<meta name="twitter:card" content="summary_large_image">

To generate these tags per page, use the meta tag generator.

The Favicon Set

Optional, but expected. A complete favicon setup has four parts. Leave them out and you get a blank tab icon, a screenshot-style tile on iOS home screens and generic Android install icons.

Keep both <link rel="icon"> elements, the favicon.ico one and the favicon.svg one. Browsers that can’t use SVG favicons, including Safari before version 26, then still get the ICO file.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    circle { fill: #1a1a2e; }
    @media (prefers-color-scheme: dark) {
      circle { fill: #f5f5f5; }
    }
  </style>
  <circle cx="16" cy="16" r="14"/>
</svg>
{
  "name": "Site name",
  "short_name": "Site",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}

For more on generating each file, see how to add a favicon to your website.

color-scheme and theme-color

Optional, recommended if you ship dark mode. <meta name="color-scheme" content="light dark"> tells the browser that the page supports both modes before any CSS arrives. Default backgrounds, form controls and scrollbars then follow the user’s preference. Without it, a dark-themed site can flash a white canvas on load. See MDN on color-scheme.

Optional. theme-color tints browser UI, and it accepts a media attribute so you can set separate light and dark values. It works in Chromium on Android, but MDN lists browser support as limited, so treat it as a nice extra rather than something every browser will show.

<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#111111" media="(prefers-color-scheme: dark)">

A Reduced-Motion Default

Optional, strongly recommended. A reduced-motion default is a rule for your base stylesheet, not a head tag. It belongs in the boilerplate because nobody goes back and adds it later. A prefers-reduced-motion: reduce block shortens animations and transitions and turns off smooth scrolling for users who have asked their operating system for less motion.

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

The durations are set to 0.01ms rather than 0, so animationend and transitionend still fire. That way, JavaScript that waits for those events doesn’t hang. This is a blunt default. Individual components can opt back in to gentle motion, such as opacity fades, that doesn’t cause vestibular problems.

Which Lines Should You Delete From Old Boilerplates?

Delete. Four lines in old HTML boilerplates come from IE-era templates and early responsive starters, and none of them does anything useful today:

  • X-UA-Compatible chose Internet Explorer document modes. No browser you support reads it.
  • maximum-scale, minimum-scale and user-scalable=no stop people with low vision from pinch-zooming, which works against WCAG 1.4.4 Resize Text. Remove them from every viewport tag.
  • shrink-to-fit=no worked around an old iOS Safari scaling behavior and no longer does anything.
  • type="text/javascript" repeats the default. The HTML spec tells authors to leave type off for classic scripts.
<!-- Before -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no">
<script type="text/javascript" src="/app.js"></script>

<!-- After -->
<meta name="viewport" content="width=device-width">
<script type="module" src="/app.js"></script>

A type="module" script is deferred by default, so it runs after parsing without blocking render. If a script can’t be a module, add defer to it.

The Full HTML Boilerplate

The complete modern HTML boilerplate below combines every required and recommended line from this article. Add site-specific extras such as feeds or preloads on top of it as needed. The skip link and the <main> landmark go in the body, and 10 essential HTML elements covers the semantic elements that come next.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <!-- Optional, test with large OS text first: <meta name="text-scale" content="scale"> -->
  <title>Page title – Site name</title>
  <meta name="description" content="One or two sentences describing this page.">
  <link rel="canonical" href="https://example.com/page">

  <meta name="color-scheme" content="light dark">
  <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
  <meta name="theme-color" content="#111111" media="(prefers-color-scheme: dark)">

  <link rel="stylesheet" href="/styles.css">
  <script type="module" src="/app.js"></script>

  <link rel="icon" href="/favicon.ico">
  <link rel="icon" href="/favicon.svg" type="image/svg+xml">
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
  <link rel="manifest" href="/site.webmanifest">

  <meta property="og:title" content="Page title">
  <meta property="og:description" content="Description for link previews.">
  <meta property="og:image" content="https://example.com/og-image.jpg">
  <meta property="og:url" content="https://example.com/page">
  <meta name="twitter:card" content="summary_large_image">
</head>
<body>
  <a href="#main">Skip to content</a>
  <main id="main"></main>
</body>
</html>
LineWhat it doesWhat breaks without itVerdict
DoctypeStandards modeQuirks-mode layoutRequired
langPronunciation, hyphenation, translationWrong screen reader voiceRequired
charsetDeclares UTF-8Garbled charactersRequired
viewportDevice-width layoutShrunken desktop layout on phonesRequired
<title>Names the pageIdentical tabs and bookmarksRequired
Description, canonicalSnippet and preferred URLWeaker snippets, duplicate URLsOptional
Open Graph, twitter:cardLink previewsBare or generic cardsOptional
Favicon setTab and home screen iconsBlank or screenshot iconsOptional
color-schemeNative dark modeWhite flash, light controlsOptional
X-UA-Compatible, zoom limits, shrink-to-fit, type="text/javascript"Legacy workaroundsNothingDelete

Only five lines in an HTML boilerplate are required. Everything else is either optional, depending on whether the page gets shared, installed or themed, or a legacy line to delete. Open the index.html of your current project, check each line against the table above, and remove anything in the delete row before you copy the file into the next project.

FAQs

Is meta charset the same as meta http-equiv Content-Type?

Yes. Under the WHATWG HTML spec, the http-equiv Content-Type version does the same job as the charset attribute. Both count as a character encoding declaration and both must sit within the first 1024 bytes. A document must not contain both, and validators flag it as an error. Use the shorter meta charset='UTF-8'. If the server sends a charset in its HTTP Content-Type header, make sure it also says UTF-8.

Do meta and link tags need a trailing slash in HTML?

No. Meta, link, img and the other void elements have no closing tag, and the HTML parser ignores a trailing slash on them. A tag written with the slash and the same tag written without it produce the same element. The slash is valid but has no effect, so it comes down to style. The only case where it matters is a page served as XHTML with an XML MIME type.

Can I leave out the html, head and body tags?

Yes. The HTML spec marks the html, head and body tags as optional in many cases, and the parser creates those elements even when the tags are missing. A boilerplate should still write them out. The lang attribute needs an explicit html start tag to sit on, and explicit tags keep the boundary between head and body easy to read. If you omit the html tag, you also lose lang.

How do I change the default lang value in Emmet's ! snippet?

Set the lang variable in VS Code's emmet.variables setting. In settings.json, add a lang key with a value such as de to the emmet.variables object, and Emmet then writes that language code every time it expands the ! snippet. The same setting has a charset variable, which you should leave at UTF-8. The viewport line still contains initial-scale=1.0, so delete that by hand or define a custom snippet.

Open-source session replay

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.