Semantic Elements
Document Structure
HTML5 introduced semantic elements that describe their meaning to both browser and developer. header, nav, main, article, aside, footer define document structure. DOCTYPE html triggers standards mode, lang improves accessibility and SEO, and charset UTF-8 is required for proper encoding.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Title</title>
</head>
<body>
<header>Header content</header>
<nav>Navigation</nav>
<main>
<article>Article content</article>
<aside>Sidebar</aside>
</main>
<footer>Footer</footer>
</body>
</html>Header & Footer
header represents introductory content (logos, titles, search forms) and may appear multiple times. footer holds ending content like copyright, links, or contact info. Both can be used within sectioning elements like article or section, not just at the page level.
<header>
<h1>Site Title</h1>
<p>Tagline</p>
</header>
<footer>
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<small>© 2024 Example</small>
</footer>Navigation Element
nav is reserved for major navigation blocks. Not every list of links needs nav—only groups significant for site navigation. Use aria-label to distinguish multiple nav elements (e.g., main vs breadcrumb). aria-current indicates the current page.
<nav aria-label="Main navigation">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li aria-current="page">Post</li>
</ol>
</nav>Main & Article
main wraps the dominant content of the page and should appear only once per page (mapped to the 'main' ARIA landmark). article is a self-contained composition—a blog post, news story, or widget—reusable independently. section groups thematically related content, always with a heading.
<body>
<header>Site header</header>
<main>
<article>
<h1>Blog Post Title</h1>
<p>Body content...</p>
<section>
<h2>Subsection</h2>
<p>More content</p>
</section>
</article>
</main>
</body>Section & Aside
section groups related content and should always include a heading. aside represents content tangentially related to the main content—sidebars, pull quotes, or ads. Both improve the document outline. Use aria-labelledby to give sections an accessible name.
<section aria-labelledby="features-heading">
<h2 id="features-heading">Features</h2>
<p>Feature description</p>
</section>
<aside aria-label="Related links">
<h2>Related</h2>
<ul>
<li><a href="/post-2">Next post</a></li>
</ul>
</aside>Figure & Figcaption
figure groups content (images, code listings, quotes) referenced from the main flow, with figcaption providing a caption. figcaption must be the first or last child. figure is not limited to images—it works for any self-contained content like code snippets or blockquotes.
<figure>
<img src="chart.png" alt="Sales chart showing growth">
<figcaption>Figure 1: Quarterly sales growth in 2024.</figcaption>
</figure>
<figure>
<blockquote>
<p>The best way to predict the future is to invent it.</p>
</blockquote>
<figcaption>— Alan Kay</figcaption>
</figure>Form Enhancements
New Input Types
HTML5 added input types like email, url, tel, date, time, color, range, number, month, week, and datetime-local. Browsers provide native validation and specialized UI (date pickers, color pickers). type=email/url trigger automatic format validation on form submission.
<form>
<label>Email: <input type="email" required></label>
<label>URL: <input type="url"></label>
<label>Phone: <input type="tel"></label>
<label>Date: <input type="date"></label>
<label>Time: <input type="time"></label>
<label>Color: <input type="color"></label>
<label>Range: <input type="range" min="0" max="100"></label>
<label>Number: <input type="number" min="0" step="0.01"></label>
<button>Submit</button>
</form>Datalist Autocomplete
datalist provides autocomplete suggestions for an input via the list attribute. Unlike select, users can type free-form text or pick a suggestion. Each option's value becomes a suggestion. datalist degrades gracefully on unsupported browsers as a plain text input.
<label>
Choose a browser:
<input list="browsers" name="browser">
</label>
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Safari">
<option value="Edge">
</datalist>Output Element
output represents the result of a calculation or user action. It has a value property accessible via JS and a for attribute referencing the inputs that contributed. It is a form-associated element, so its value is submitted with the form. Great for live calculators.
<form oninput="result.value = (+a.value + +b.value)">
<input type="number" name="a" value="0">
+
<input type="number" name="b" value="0">
=
<output name="result" for="a b">0</output>
</form>Progress & Meter
progress shows completion of a task (value/max); omit value for an indeterminate state. meter displays a scalar measurement within a known range, with low/high/optimum thresholds that affect its color. meter is read-only and not for progress indication.
<label>Uploading:
<progress value="70" max="100">70%</progress>
</label>
<label>Disk usage:
<meter value="0.6" min="0" max="1" low="0.4" high="0.8" optimum="0.2">
60%
</meter>
</label>
<!-- Indeterminate progress -->
<progress>Processing...</progress>Form Attribute & Fieldset
The form attribute associates an input, fieldset, or button with a form by id, even when the element is not nested inside it. fieldset groups related controls and legend provides a caption. The disabled attribute on fieldset disables all contained controls at once.
<form id="myForm" action="/submit" method="post"></form>
<!-- Input outside the form, but associated via form attribute -->
<label>
Name:
<input type="text" name="name" form="myForm" required>
</label>
<fieldset form="myForm" disabled>
<legend>Shipping Options</legend>
<label><input type="radio" name="ship" value="standard"> Standard</label>
<label><input type="radio" name="ship" value="express"> Express</label>
</fieldset>
<button type="submit" form="myForm">Submit</button>Placeholder & Autofocus
placeholder shows a hint inside the input and disappears on focus—it is not a replacement for a label. autofocus focuses the element on page load (use sparingly, one per page). autocomplete controls whether the browser remembers past values. title provides a tooltip but should not replace accessible labels.
<form>
<label>
Search:
<input type="search" name="q"
placeholder="Type to search..."
autofocus
autocomplete="off">
</label>
<label>
Hint text:
<input type="text" name="hint" title="Helpful tooltip">
</label>
<button type="submit">Go</button>
</form>Form Validation
Required Attribute
The boolean required attribute marks a field as mandatory; the browser blocks submission and shows an error bubble if it is empty. It works on all text, number, checkbox, radio, file, and select controls. Pair it with :invalid and :valid CSS pseudo-classes for visual feedback.
<form>
<label>
Email (required):
<input type="email" name="email" required>
</label>
<label>
Optional phone:
<input type="tel" name="phone">
</label>
<button>Submit</button>
</form>Pattern Validation
pattern accepts a JavaScript regular expression (no slashes) the value must match. Anchoring with ^ and $ is recommended. The title attribute gives users a hint on failure. Pattern is implicitly anchored for type=email/url but explicit for text-based inputs.
<form>
<label>
Product code:
<input type="text" name="code"
pattern="[A-Z]{3}-\d{4}"
title="Three letters, hyphen, four digits (e.g., ABC-1234)"
required>
</label>
<label>
Hex color:
<input type="text" name="color" pattern="^#[0-9A-Fa-f]{6}$">
</label>
<button>Submit</button>
</form>Min, Max & Step
min and max define the lower and upper bounds for numeric and date inputs. step specifies the increment; values must be min + n*step. step='any' allows any decimal. The browser rejects out-of-range values on submit and adjusts spinner clicks.
<form>
<label>
Quantity:
<input type="number" name="qty" min="1" max="99" step="1" value="1">
</label>
<label>
Price:
<input type="number" name="price" min="0" step="0.01">
</label>
<label>
Date range:
<input type="date" name="start" min="2024-01-01" max="2024-12-31">
</label>
<button>Submit</button>
</form>Minlength & Maxlength
minlength and maxlength constrain the number of characters for text inputs and textareas. minlength triggers validation only if the field has a value (combine with required for mandatory fields). maxlength hard-traps input beyond the limit, preventing typing.
<form>
<label>
Username (3-20 chars):
<input type="text" name="user" minlength="3" maxlength="20" required>
</label>
<label>
Bio (max 280 chars):
<textarea name="bio" maxlength="280"></textarea>
</label>
<button>Submit</button>
</form>Constraint Validation API
The Constraint Validation API exposes validity.valid, valueMissing, typeMismatch, patternMismatch, tooShort, rangeOverflow, and more. setCustomValidity(msg) shows a custom message (pass empty string to clear). checkValidity() returns a boolean and fires the invalid event; reportValidity() also shows the UI.
<form id="f">
<label>
Username:
<input name="u" required minlength="3">
</label>
<button>Submit</button>
</form>
<script>
const input = document.querySelector('#f input[name=u]');
input.addEventListener('input', () => {
if (input.validity.valueMissing) {
input.setCustomValidity('Please pick a username.');
} else if (input.validity.tooShort) {
input.setCustomValidity('At least 3 characters.');
} else {
input.setCustomValidity('');
}
});
</script>novalidate & formnovalidate
novalidate on the form disables browser validation entirely, useful when you validate via JS. formnovalidate on a submit button skips validation only for that button's submission (e.g., a 'Save draft' that allows incomplete data). Both still submit the form.
<!-- Browser validation disabled for whole form -->
<form novalidate>
<label>Email: <input type="email" name="e" required></label>
<button>Save draft</button>
<!-- Skip validation only for this button -->
<button type="submit" formnovalidate>Cancel</button>
</form>Canvas Drawing
Canvas Element & 2D Context
canvas is a bitmap drawing surface. getContext('2d') returns the 2D rendering context; 'webgl' returns a 3D context. The width/height attributes set the drawing buffer size (not CSS size). For crisp rendering on retina displays, scale the buffer by devicePixelRatio. Fallback content goes between the tags.
<canvas id="cv" width="400" height="300">
Your browser does not support canvas.
</canvas>
<script>
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
// High-DPI scaling
const dpr = window.devicePixelRatio || 1;
canvas.width = 400 * dpr;
canvas.height = 300 * dpr;
ctx.scale(dpr, dpr);
</script>Drawing Rectangles
fillRect(x, y, w, h) draws a filled rectangle, strokeRect draws only the outline, and clearRect erases pixels to transparent. fillStyle and strokeStyle accept colors, gradients, or patterns. lineWidth affects the stroke thickness, centered on the path edge.
<script>
const ctx = canvas.getContext('2d');
// Filled rectangle
ctx.fillStyle = '#3498db';
ctx.fillRect(10, 10, 100, 50);
// Outlined rectangle
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 3;
ctx.strokeRect(130, 10, 100, 50);
// Clear a rectangle (erase)
ctx.clearRect(50, 20, 40, 30);
</script>Paths & Lines
beginPath starts a new path. moveTo sets the pen position without drawing; lineTo adds a line segment. closePath connects the last point to the start. Call fill() and/or stroke() to render. Arcs (arc), curves (quadraticCurveTo, bezierCurveTo), and rects can also be added to a path.
<script>
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(20, 20); // starting point
ctx.lineTo(180, 20); // top edge
ctx.lineTo(100, 140); // bottom corner
ctx.closePath(); // back to start
ctx.fillStyle = 'rgba(46,204,113,0.6)';
ctx.fill();
ctx.stroke();
</script>Drawing Text
font uses the same shorthand as CSS font. textAlign (start, end, left, right, center) and textBaseline (top, middle, alphabetic, bottom) control alignment. fillText draws solid text; strokeText draws only the outline. measureText('foo').width returns the rendered width.
<script>
const ctx = canvas.getContext('2d');
ctx.font = '600 24px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#2c3e50';
ctx.fillText('Hello Canvas', 200, 150);
ctx.lineWidth = 1;
ctx.strokeStyle = '#bdc3c7';
ctx.strokeText('Outlined', 200, 200);
</script>Drawing Images
drawImage can take an img, canvas, or video element. With 3 args it draws at natural size; with 5 args it scales to w/h; with 9 args the first four are source-rectangle (crop) and the last four are destination-rectangle. Wait for onload before drawing, or the image may not appear.
<script>
const img = new Image();
img.src = 'pic.jpg';
img.onload = () => {
ctx.drawImage(img, 0, 0); // full size
ctx.drawImage(img, 0, 0, 100, 75); // scaled
// Source crop -> destination
ctx.drawImage(img, 32, 32, 64, 64, 200, 0, 64, 64);
};
</script>Gradients
createLinearGradient(x0, y0, x1, y1) and createRadialGradient(x0, y0, r0, x1, y1, r1) define gradients. addColorStop(offset, color) adds color stops with offset from 0 to 1. Always pass standard color strings; mismatched formats (mixing hex and rgba) can break interpolation. Assign the gradient to fillStyle or strokeStyle.
<script>
// Linear gradient
const lg = ctx.createLinearGradient(0, 0, 200, 0);
lg.addColorStop(0, '#1e90ff');
lg.addColorStop(1, '#ffffff');
ctx.fillStyle = lg;
ctx.fillRect(0, 0, 200, 80);
// Radial gradient
const rg = ctx.createRadialGradient(100, 180, 5, 100, 180, 80);
rg.addColorStop(0, 'rgba(255,200,0,1)');
rg.addColorStop(1, 'rgba(255,200,0,0)');
ctx.fillStyle = rg;
ctx.fillRect(0, 100, 200, 160);
</script>Inline SVG
SVG Element Basics
Inline SVG uses the svg element with width/height for display size and viewBox for the coordinate system. xmlns is required when the SVG is used standalone, optional inline. SVG is vector-based: it scales crisply at any size and is searchable and styleable with CSS like normal HTML.
<svg width="200" height="120" viewBox="0 0 200 120"
xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="180" height="100"
fill="#3498db" rx="8" />
<text x="100" y="65" text-anchor="middle"
fill="white" font-size="18">SVG</text>
</svg>Basic Shapes
SVG primitives include rect, circle, ellipse, line, polyline (open), and polygon (closed). cx/cy are center coordinates; r is radius. Fill is the interior color, stroke is the outline. stroke-width sets outline thickness. All attributes can also be set via CSS.
<svg width="240" height="120" viewBox="0 0 240 120">
<rect x="10" y="10" width="80" height="80" fill="#e74c3c" />
<circle cx="150" cy="50" r="40" fill="#2ecc71" />
<ellipse cx="200" cy="60" rx="30" ry="18" fill="#f1c40f" />
<line x1="10" y1="110" x2="230" y2="110" stroke="#333" stroke-width="2" />
<polyline points="10,100 60,70 110,100 160,70"
fill="none" stroke="#9b59b6" stroke-width="3" />
</svg>Paths
path is the most powerful SVG element, drawing via a d attribute of commands. M = moveTo, L = lineTo, H/V = horizontal/vertical lines, C = cubic bezier, Q = quadratic bezier, A = arc, Z = close path. Lowercase letters use relative coordinates. Most editor-exported SVGs use paths.
<svg width="200" height="120" viewBox="0 0 200 120">
<!-- M=moveTo, L=lineTo, C=cubicBezier, Z=closePath -->
<path d="M 10 80
C 40 10, 90 10, 120 80
L 180 80 Z"
fill="none" stroke="#3498db" stroke-width="3" />
<!-- A=arc -->
<path d="M 10 100 A 90 90 0 0 1 190 100"
fill="none" stroke="#e74c3c" stroke-width="2" />
</svg>Text in SVG
text renders scalable, selectable text. x/y position the baseline. text-anchor (start/middle/end) and dominant-baseline control alignment. tspan allows per-segment styling, positioning, and line breaks. SVG text stays crisp at any zoom and is accessible, unlike text drawn on canvas.
<svg width="300" height="100" viewBox="0 0 300 100">
<text x="20" y="40" font-family="system-ui" font-size="20"
fill="#2c3e50">Plain text</text>
<text x="20" y="75" font-size="20" fill="#e74c3c"
text-decoration="underline">Underlined</text>
<text x="180" y="50">
<tspan fill="#3498db">Blue</tspan>
<tspan fill="#27ae60">Green</tspan>
</text>
</svg>Gradients & Patterns
Gradients live inside defs and are referenced by id via fill='url(#id)'. linearGradient uses x1/y1/x2/y2 (objectBoundingBox by default, 0-1). radialGradient defaults to a centered circle. stop elements define color stops. defs also holds patterns, clip paths, and reusable symbols.
<svg width="220" height="120" viewBox="0 0 220 120">
<defs>
<linearGradient id="g1" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e90ff" />
<stop offset="100%" stop-color="#ffffff" />
</linearGradient>
<radialGradient id="g2">
<stop offset="0%" stop-color="#f1c40f" />
<stop offset="100%" stop-color="#e67e22" />
</radialGradient>
</defs>
<rect x="10" y="10" width="90" height="90" fill="url(#g1)" />
<circle cx="160" cy="55" r="45" fill="url(#g2)" />
</svg>Inline SVG vs Canvas
SVG is resolution-independent, each shape is a DOM node you can style and attach events to—ideal for icons, charts with few elements, and accessible graphics. Canvas is a single bitmap, faster for thousands of objects or pixel manipulation (games, image processing), but not scalable or DOM-accessible.
<!-- SVG: vector, DOM-accessible, scalable -->
<svg width="100" height="100" viewBox="0 0 100 100">
<circle id="c" cx="50" cy="50" r="40" fill="#3498db" />
</svg>
<button onclick="document.getElementById('c')
.setAttribute('r','20')">Shrink</button>
<!-- Canvas: bitmap, pixel-based, faster for many objects -->
<canvas id="cv" width="100" height="100"></canvas>Audio & Video
Video Element
video embeds video without plugins. controls shows native UI; poster is the thumbnail before playback. Multiple source elements let the browser pick the first supported codec. Fallback text between the tags shows on unsupported browsers. Always include width/height to avoid layout shift.
<video controls width="640" poster="cover.jpg">
<source src="movie.mp4" type="video/mp4">
<source src="movie.webm" type="video/webm">
Your browser does not support the video tag.
</video>Audio Element
audio works like video without visual dimensions. controls adds play/pause/seek/volume UI. Without controls, the element is invisible and must be controlled via JS. Provide multiple sources (mp3, ogg) for cross-browser support. autoplay may be blocked by browsers unless muted.
<audio controls>
<source src="song.mp3" type="audio/mpeg">
<source src="song.ogg" type="audio/ogg">
Your browser does not support audio.
</audio>
<!-- Simple single-source -->
<audio src="beep.mp3" autoplay></audio>Multiple Sources
Browsers choose the first source whose type they can play. Including the codecs parameter helps them decide without downloading. List modern formats (webm/av1) first for smaller payloads, mp4 as the universal fallback. A download link inside the tags is a graceful fallback.
<video controls>
<source src="clip.webm" type='video/webm; codecs="vp9, opus"'>
<source src="clip.mp4" type='video/mp4; codecs="hvc1"'>
<source src="clip.ogv" type="video/ogg">
<p>Download <a href="clip.mp4">clip.mp4</a></p>
</video>Track Element (Captions)
track adds timed text tracks via WebVTT (.vtt) files. kind can be subtitles, captions, descriptions, chapters, or metadata. srclang declares the language, label is shown in the menu, and default picks the initial track. Captions are essential for accessibility and improve SEO.
<video controls>
<source src="talk.mp4" type="video/mp4">
<track kind="subtitles" src="talk.en.vtt"
srclang="en" label="English" default>
<track kind="captions" src="talk.en.cc.vtt"
srclang="en" label="English CC">
<track kind="chapters" src="talk.chapters.vtt"
srclang="en" label="Chapters">
</video>Media Attributes
autoplay starts playback automatically but is blocked unless muted is also set. loop repeats forever. playsinline prevents forced fullscreen on iOS (essential for inline backgrounds). preload can be none, metadata (just duration/dimensions), or auto (buffer the whole file). Avoid autoplay+preload=auto together to save bandwidth.
<video src="bg.mp4"
autoplay muted loop
playsinline
preload="auto"
width="1280" height="720">
</video>
<video src="preview.mp4"
controls
preload="metadata"
poster="preview.jpg">
</video>Media API
The HTMLMediaElement API exposes play(), pause(), load(), currentTime, duration, volume, muted, playbackRate, and readyState. Events include play, pause, timeupdate, ended, volumechange, loadedmetadata, and waiting. Build custom players by hiding controls and wiring buttons to these methods.
<video id="v" src="movie.mp4"></video>
<button onclick="v.play()">Play</button>
<button onclick="v.pause()">Pause</button>
<input type="range" min="0" max="100" oninput="v.volume = this.value/100">
<script>
const v = document.getElementById('v');
v.addEventListener('timeupdate', () => {
console.log(v.currentTime, '/', v.duration);
});
v.addEventListener('ended', () => alert('Done!'));
// Jump 10s forward
function skip() { v.currentTime += 10; }
</script>Geolocation
getCurrentPosition
getCurrentPosition takes success and optional error callbacks plus an options object. The position object exposes coords.latitude, longitude, accuracy, and—when available—altitude, heading, and speed. Geolocation only works on HTTPS (or localhost) and prompts the user for permission.
<script>
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(pos) => {
console.log('Latitude:', pos.coords.latitude);
console.log('Longitude:', pos.coords.longitude);
console.log('Accuracy:', pos.coords.accuracy, 'meters');
},
(err) => console.error(err.message),
{ enableHighAccuracy: true, timeout: 10000 }
);
}
</script>Position Options
enableHighAccuracy asks for the most precise result (GPS) at the cost of time and battery. timeout caps the wait; if exceeded, error.code is TIMEOUT (3). maximumAge lets the browser return a cached position if it is younger than the given milliseconds, avoiding repeated GPS calls.
<script>
navigator.geolocation.getCurrentPosition(success, error, {
enableHighAccuracy: true, // use GPS, slower
timeout: 5000, // max wait in ms
maximumAge: 60000 // accept cached fix up to 60s old
});
function success(pos) { /* ... */ }
function error(err) { /* ... */ }
</script>watchPosition
watchPosition calls the success callback every time the position changes, returning an ID. Pass that ID to clearWatch to stop updates. This is essential for navigation apps. Be mindful of battery—use a reasonable update interval and stop watching when not needed.
<script>
const watchId = navigator.geolocation.watchPosition(
(pos) => updateMap(pos.coords),
(err) => console.warn(err.message),
{ enableHighAccuracy: true }
);
// Stop tracking
document.getElementById('stop').onclick = () => {
navigator.geolocation.clearWatch(watchId);
};
</script>Error Handling
The error callback receives a GeolocationPositionError with code and message. PERMISSION_DENIED (1) means the user declined or the browser blocked it. POSITION_UNAVAILABLE (2) means the device could not determine a fix. TIMEOUT (3) means the request took longer than the timeout option. Always handle all three cases gracefully.
<script>
navigator.geolocation.getCurrentPosition(
(pos) => console.log(pos),
(err) => {
switch (err.code) {
case err.PERMISSION_DENIED: // 1
console.log('User denied permission'); break;
case err.POSITION_UNAVAILABLE: // 2
console.log('Position unavailable'); break;
case err.TIMEOUT: // 3
console.log('Timed out'); break;
}
console.log(err.message);
}
);
</script>Permissions API
The Permissions API lets you check the state of a permission without triggering a prompt. query({ name: 'geolocation' }) returns a PermissionStatus whose state is granted, denied, or prompt. Listen to onchange to react when the user grants or revokes permission elsewhere. Supported for geolocation, notifications, camera, microphone, and others.
<script>
navigator.permissions
.query({ name: 'geolocation' })
.then((result) => {
// result.state: 'granted' | 'denied' | 'prompt'
if (result.state === 'granted') {
startTracking();
} else if (result.state === 'prompt') {
showEnableButton();
} else {
showInstructions();
}
result.onchange = () => console.log(result.state);
});
</script>Web Storage
localStorage Basics
localStorage stores string key/value pairs with no expiration; data persists across tabs and sessions. Values are strings, so numbers and booleans are auto-converted. The limit is ~5MB per origin. Storage is synchronous and blocks the main thread, so avoid large writes. Accessing it in private mode may throw, so wrap in try/catch.
<script>
// Store a value (strings only)
localStorage.setItem('theme', 'dark');
// Read a value
const theme = localStorage.getItem('theme'); // 'dark'
// Remove one key
localStorage.removeItem('theme');
// Remove everything
localStorage.clear();
// Number of stored keys
console.log(localStorage.length);
</script>sessionStorage
sessionStorage has the same API as localStorage but is scoped to a single tab and cleared when the tab closes. It is useful for transient data like a multi-step form draft, a temporary shopping cart, or CSRF tokens. Data is not shared between tabs even on the same origin.
<script>
// sessionStorage works like localStorage but per-tab
sessionStorage.setItem('cart', '3 items');
console.log(sessionStorage.getItem('cart'));
sessionStorage.removeItem('cart');
// Survives a page reload but not a tab close
// Also isolated per tab (not shared)
</script>JSON Data Storage
Since Web Storage only stores strings, use JSON.stringify and JSON.parse for objects and arrays. Always wrap parse in try/catch because corrupted or manually edited data will throw. For complex data, version the key (e.g., 'settings.v2') so schema changes don't break old data.
<script>
const settings = { theme: 'dark', fontSize: 16, lang: 'en' };
// Serialize before saving
localStorage.setItem('settings', JSON.stringify(settings));
// Parse when reading
try {
const saved = JSON.parse(localStorage.getItem('settings'));
console.log(saved.theme); // 'dark'
} catch (e) {
console.warn('Corrupted data', e);
}
</script>Storage Events
The storage event fires in every other tab of the same origin when localStorage or sessionStorage changes—but NOT in the tab that made the change. It is the simplest cross-tab communication channel. The event object carries key, oldValue, newValue, url, and storageArea. Use it to sync logout, theme changes, or live updates across tabs.
<script>
// Listen in OTHER tabs (same origin) for changes
window.addEventListener('storage', (e) => {
console.log('Key changed:', e.key);
console.log('Old value:', e.oldValue);
console.log('New value:', e.newValue);
console.log('URL:', e.url);
});
// The tab that calls setItem does NOT receive the event
localStorage.setItem('count', '42');
</script>Clearing Storage
clear() removes every key for the origin—use sparingly and confirm with the user. key(n) returns the key name at index n, or null if out of range. Iteration order is implementation-defined, so don't rely on it. For partial cleanup, list keys with a prefix (e.g., 'cache:') and removeItem them in a loop.
<script>
// Remove a single key
localStorage.removeItem('temp');
// Wipe all keys for this origin
localStorage.clear();
// Iterate keys
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
keys.push(localStorage.key(i));
}
console.log(keys);
// Indexed access (order is not guaranteed)
localStorage.key(0);
</script>IndexedDB
Opening a Database
indexedDB.open(name, version) opens or creates a database. onupgradeneeded fires when the version increases or the DB is first created—it is the ONLY place you can create or alter object stores and indexes. Bump the version number whenever you change the schema. Stores need a keyPath (the property used as the key) or an auto-incrementing key.
<script>
const request = indexedDB.open('MyDB', 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('email', 'email', { unique: true });
}
};
request.onsuccess = (e) => {
const db = e.target.result;
console.log('DB ready:', db.name, db.version);
};
request.onerror = (e) => console.error(e.target.error);
</script>Object Stores
An object store is like a table; each record has a key and a value (any structured-clone-able object). keyPath tells the store which property holds the key. autoIncrement assigns sequential integer keys. Indexes let you query by a non-key property; unique: true enforces no duplicates on that field.
<script>
request.onupgradeneeded = (e) => {
const db = e.target.result;
// With a key path
const users = db.createObjectStore('users', { keyPath: 'id' });
// Auto-incrementing integer key
const logs = db.createObjectStore('logs', { autoIncrement: true });
// Indexes for fast lookups
users.createIndex('name', 'name', { unique: false });
users.createIndex('email', 'email', { unique: true });
};
</script>Adding Data
All operations happen inside a transaction created with db.transaction(storeName, mode). Mode is 'readonly' (default) or 'readwrite'. add() rejects duplicates; put() upserts. The transaction auto-commits when all requests complete; listen to oncomplete. Keep transactions short—they block other transactions on the same stores.
<script>
function addUser(db, user) {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.add(user); // throws on duplicate key
// store.put(user); // inserts OR overwrites
tx.oncomplete = () => console.log('Saved');
tx.onerror = () => console.error(tx.error);
}
addUser(db, { id: 1, name: 'Ada', email: '[email protected]' });
</script>Reading Data
get(key) reads a single record by primary key; getAll() returns all records as an array. To look up by a non-key field, open the index with store.index(name) and call get/getAll on it. Each request has onsuccess and onerror. Results are in event.target.result or request.result.
<script>
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
// By primary key
const req = store.get(1);
req.onsuccess = () => console.log(req.result);
// By index
const byEmail = store.index('email');
byEmail.get('[email protected]').onsuccess = (e) => {
console.log(e.target.result);
};
// All records
store.getAll().onsuccess = (e) => {
console.log(e.target.result); // array
};
</script>Cursors & Indexes
openCursor() lets you iterate records one at a time, ideal for large datasets that won't fit in memory. Call cursor.continue() to advance. cursor.update(value) modifies the current record; cursor.delete() removes it. IDBKeyRange (bound, only, lowerBound, upperBound) scopes index queries—useful for prefix or range searches.
<script>
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
// Iterate and modify
const req = store.openCursor();
req.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
if (cursor.value.name === 'Ada') {
cursor.value.role = 'admin';
cursor.update(cursor.value);
}
cursor.continue();
}
};
// Range query on an index
const range = IDBKeyRange.bound('A', 'M');
store.index('name').openCursor(range).onsuccess = (e) => { /* ... */ };
</script>Drag and Drop API
Making Elements Draggable
Set draggable='true' on any element to make it draggable. The dragstart event fires when dragging begins; use dataTransfer.setData to package data for the drop target. effectAllowed (copy, move, link, all, none) hints at the intended operation. dragend fires when the drag finishes, regardless of whether it dropped.
<div id="card" draggable="true">
Drag me!
</div>
<script>
const card = document.getElementById('card');
card.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', card.id);
e.dataTransfer.effectAllowed = 'move';
card.classList.add('dragging');
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
});
</script>Drag Events
To accept a drop, you MUST call preventDefault() on both dragover and drop—otherwise the browser does nothing. dragenter/dragleave fire when the pointer enters/leaves the target. dropEffect (copy, move, link, none) controls the cursor. The drag event fires continuously on the source during dragging.
<script>
// Events on the SOURCE element:
// dragstart, drag, dragend
// Events on the TARGET element:
// dragenter, dragover, dragleave, drop
const drop = document.getElementById('drop');
drop.addEventListener('dragover', (e) => {
e.preventDefault(); // REQUIRED to allow drop
e.dataTransfer.dropEffect = 'move';
});
drop.addEventListener('drop', (e) => {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
drop.appendChild(document.getElementById(id));
});
</script>DataTransfer
dataTransfer carries data between the drag source and drop target. setData(format, data) stores a string; getData(format) reads it. Standard formats include text/plain, text/uri-list, text/html, and files. You can also register custom MIME types. Some browsers restrict getData to drop events for security.
<script>
card.addEventListener('dragstart', (e) => {
// Multiple data types
e.dataTransfer.setData('text/plain', 'Hello');
e.dataTransfer.setData('text/uri-list', 'https://example.com');
e.dataTransfer.setData('application/json', JSON.stringify({ a: 1 }));
// Read in the drop handler
// e.dataTransfer.getData('text/plain')
});
</script>Drop Zone
A common pattern: highlight the drop zone on dragenter/dragover, clear it on dragleave/drop. dataTransfer.files gives a FileList when dragging files from the OS, enabling drag-and-drop uploads. Always preventDefault on dragover or the drop event will not fire. Use dragenter/leave counters if you have nested elements.
<style>
.dropzone { border: 2px dashed #bbb; padding: 24px; }
.dropzone.over { background: #eaf6ff; border-color: #3498db; }
</style>
<div class="dropzone" id="dz">Drop files here</div>
<script>
const dz = document.getElementById('dz');
dz.addEventListener('dragenter', () => dz.classList.add('over'));
dz.addEventListener('dragover', (e) => e.preventDefault());
dz.addEventListener('dragleave', () => dz.classList.remove('over'));
dz.addEventListener('drop', (e) => {
e.preventDefault();
dz.classList.remove('over');
const files = e.dataTransfer.files; // FileList
for (const f of files) console.log(f.name, f.size);
});
</script>Drag Image
setDragImage(element, offsetX, offsetY) replaces the default drag ghost with a custom element. The element must be in the DOM and rendered, so create it, append, then remove after dragstart. Offsets position the cursor relative to the image's top-left. If you pass no image, the browser screenshots the dragged element.
<script>
card.addEventListener('dragstart', (e) => {
const ghost = document.createElement('div');
ghost.textContent = 'Dragging card';
ghost.style.cssText = 'position:absolute;top:-999px;' +
'padding:8px;background:#3498db;color:#fff;';
document.body.appendChild(ghost);
e.dataTransfer.setDragImage(ghost, 20, 20);
setTimeout(() => document.body.removeChild(ghost), 0);
});
</script>Web Workers
Worker Basics
new Worker(url) spawns a background thread that runs the script file. Communication is via postMessage and onmessage, with data copied (structured clone) between threads. Workers cannot touch the DOM, window, or document—but they can use fetch, IndexedDB, and setTimeout. Use them for heavy computation so the UI stays responsive.
<!-- main.js -->
<script>
const worker = new Worker('worker.js');
worker.postMessage({ cmd: 'sum', nums: [1, 2, 3] });
worker.onmessage = (e) => {
console.log('Result:', e.data); // 6
};
worker.onerror = (e) => {
console.error(e.message, e.filename, e.lineno);
};
</script>Worker Script
Inside a worker, self (or this) is the global scope—postMessage and onmessage hang off it. There is no window or document. Workers can use fetch, importScripts(), setTimeout/setInterval, and IndexedDB. importScripts('a.js','b.js') synchronously loads other scripts into the worker. Each worker runs in its own isolate.
// worker.js
self.onmessage = (e) => {
const { cmd, nums } = e.data;
if (cmd === 'sum') {
let total = 0;
for (const n of nums) total += n;
self.postMessage(total);
} else if (cmd === 'heavy') {
const result = doExpensiveWork();
self.postMessage(result);
}
};
function doExpensiveWork() {
// ... long-running computation
return 'done';
}Posting Messages
postMessage(data, transferables) sends data, copying it via structured clone. Pass an array of ArrayBuffer, MessagePort, or ImageBitmap in the second argument to TRANSFER ownership instead of copying—much faster for large buffers, but the original becomes unusable in the sending thread.
<script>
const w = new Worker('worker.js');
// Plain object
w.postMessage({ type: 'start' });
// Transferable: zero-copy move of a buffer
const buffer = new ArrayBuffer(1024);
w.postMessage(buffer, [buffer]);
// buffer is now "neutered" in main thread
// Two-way communication
w.onmessage = (e) => console.log('Worker said:', e.data);
</script>Transferable Objects
Transferable objects (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas) move between threads without copying. transferControlToOffscreen hands a canvas to a worker, which can then render to it independently—great for smooth animations while the main thread is busy. The main thread loses access to the transferred object.
<script>
// Send a canvas ImageBitmap to a worker for processing
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('paint.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// Now the worker draws directly to the canvas
// without round-trips to the main thread.
</script>
<!-- paint.js -->
self.onmessage = (e) => {
const ctx = e.data.canvas.getContext('2d');
ctx.fillStyle = '#3498db';
ctx.fillRect(0, 0, 100, 100);
};Shared Workers
A SharedWorker is a single worker shared by multiple tabs of the same origin. Each connection gets a MessagePort via the connect event. Use it for cross-tab state (live counts, shared caches) or a single WebSocket fan-out. SharedWorkers are not supported in Safari on iOS and have quirks; test before relying on them.
<!-- main.js -->
<script>
const sw = new SharedWorker('shared.js');
sw.port.onmessage = (e) => console.log('Broadcast:', e.data);
sw.port.start();
sw.port.postMessage({ from: 'tab-A' });
</script>
<!-- shared.js -->
const ports = new Set();
self.onconnect = (e) => {
const port = e.ports[0];
ports.add(port);
port.onmessage = (ev) => {
// Broadcast to every connected tab
for (const p of ports) p.postMessage(ev.data);
};
port.start();
};WebSockets
Opening a Connection
new WebSocket(url) opens a persistent, bidirectional connection. Use wss:// (TLS) in production—ws:// is plaintext and blocked on HTTPS pages. The readyState goes CONNECTING (0) to OPEN (1) to CLOSING (2) to CLOSED (3). Listen for open, message, error, and close events.
<script>
const ws = new WebSocket('wss://echo.example.com/chat');
ws.addEventListener('open', () => {
console.log('Connected');
ws.send('Hello server!');
});
ws.addEventListener('message', (e) => {
console.log('Received:', e.data);
});
ws.addEventListener('close', (e) => {
console.log('Closed', e.code, e.reason);
});
</script>Sending Messages
send() accepts strings, Blobs, ArrayBuffers, and ArrayBufferViews. For structured data, JSON.stringify first and parse on the other end. Messages are queued if sent before open, but to be safe check readyState === OPEN. The protocol is full-duplex—either side can send at any time.
<script>
// Strings
ws.send('plain text');
// JSON
ws.send(JSON.stringify({ type: 'chat', text: 'hi' }));
// Binary
const blob = new Blob([new Uint8Array([1,2,3])]);
ws.send(blob);
ws.send(new ArrayBuffer(8));
// Check state before sending
if (ws.readyState === WebSocket.OPEN) ws.send(data);
</script>Receiving Messages
message events deliver e.data as a string (text), Blob (binary default), or ArrayBuffer (set ws.binaryType = 'arraybuffer'). For text protocols, JSON is the most common encoding. For binary, choose Blob for files/images and ArrayBuffer for numeric parsing. Define a message schema so both sides agree on shape.
<script>
ws.addEventListener('message', (e) => {
// Text messages: e.data is a string
if (typeof e.data === 'string') {
const msg = JSON.parse(e.data);
console.log(msg.type, msg.text);
}
// Binary: e.data is a Blob or ArrayBuffer
else {
e.data.arrayBuffer().then((buf) => {
const view = new DataView(buf);
console.log(view.getInt32(0));
});
}
});
// Force binary type
ws.binaryType = 'arraybuffer';
</script>Errors & Closing
error events carry little detail for security; check the close event afterward. close(code, reason) initiates a clean shutdown; code 1000 is normal closure, 1006 means the connection dropped without a close frame (network issue). Codes 4000-4999 are reserved for application use. wasClean is true only when both sides completed the handshake.
<script>
ws.addEventListener('error', (e) => {
console.error('Socket error', e);
});
ws.addEventListener('close', (e) => {
console.log('code:', e.code, 'reason:', e.reason, 'clean:', e.wasClean);
});
// Close gracefully (1000 = normal closure)
ws.close(1000, 'goodbye');
// Common codes:
// 1000 normal, 1001 going away, 1006 abnormal (no close frame)
// 4000-4999 app-defined
</script>Reconnection
Browsers don't auto-reconnect, so implement it yourself with exponential backoff (delay = min(base * 2^retry, max)). Send periodic pings so dead sockets are detected and closed. Reconnect on close but cap the retry interval to avoid hammering the server. Reset the retry counter once connected successfully.
<script>
let ws;
let retry = 0;
function connect() {
ws = new WebSocket('wss://example.com/live');
ws.onopen = () => { retry = 0; console.log('open'); };
ws.onmessage = (e) => console.log(e.data);
ws.onclose = () => {
const delay = Math.min(1000 * 2 ** retry, 30000);
retry++;
setTimeout(connect, delay);
};
}
connect();
// Heartbeat to detect dead connections
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send('ping');
}, 30000);
</script>History API
pushState
pushState(state, title, url) adds a new entry to the session history and updates the address bar without reloading the page—the core of client-side routing. The URL must be same-origin or pushState throws. The title argument is currently ignored by browsers. The state object is serializable and retrievable via history.state.
<script>
// Add a new entry WITHOUT reloading
history.pushState(
{ page: 'about' }, // state object
'About', // title (ignored by most browsers)
'/about' // URL
);
console.log(history.length); // entries count
console.log(history.state); // { page: 'about' }
</script>replaceState
replaceState(state, title, url) works like pushState but replaces the current entry instead of adding a new one—so back/forward skip over the original. Use it to update the URL or state without polluting history, e.g., after applying a filter you don't want the user to 'go back' through.
<script>
// Replace the CURRENT entry (no new history item)
history.replaceState(
{ page: 'home', tab: 'featured' },
'',
'/home?tab=featured'
);
// Useful for: cleaning up redirects, updating URL after
// AJAX filter changes, or normalizing the entry URL.
</script>popstate Event
popstate fires when the user clicks back/forward or history.go/back/forward is called—not on pushState/replaceState. The event.state is the state object from the entry being navigated to. Most SPA routers call the render function in both navigate() and the popstate handler so all navigations update the view.
<script>
window.addEventListener('popstate', (e) => {
console.log('Location:', location.pathname);
console.log('State:', e.state);
renderRoute(location.pathname);
});
function navigate(path) {
history.pushState({ path }, '', path);
renderRoute(path);
}
// NOTE: pushState/replaceState do NOT fire popstate.
// Only back/forward (and history.go) do.
</script>Navigation Methods
back(), forward(), and go(delta) move through the session history; go(0) reloads. history.length is the number of entries in the current session, capped by the browser. These methods are asynchronous—popstate fires once navigation completes. Calling go beyond the bounds does nothing.
<script>
history.back(); // go back one entry
history.forward(); // go forward one entry
history.go(-2); // go back two entries
history.go(2); // go forward two entries
history.go(0); // reload current page
if (history.length === 1) {
console.log('First page in this tab');
}
</script>SPA Routing
A minimal SPA router: intercept internal link clicks, pushState to update the URL, and render the new view. Handle popstate so back/forward works. Use data-link or a class to mark internal links, and skip external or target=_blank links. Real frameworks add nested routes, params, guards, and scroll restoration.
<script>
document.body.addEventListener('click', (e) => {
const a = e.target.closest('a');
if (!a || !a.matches('[data-link]')) return;
if (a.origin !== location.origin) return;
e.preventDefault();
history.pushState({ path: a.pathname }, '', a.pathname);
render(a.pathname);
});
window.addEventListener('popstate', () => render(location.pathname));
function render(path) {
fetch(path, { headers: { 'X-Ajax': '1' } })
.then((r) => r.text())
.then((html) => {
document.querySelector('#app').innerHTML = html;
});
}
</script>Inline Semantic Elements
time Element
time represents a specific date, time, or duration. The machine-readable value goes in the datetime attribute (YYYY-MM-DD, HH:MM, or full ISO 8601 with timezone); the human-readable text is the content. Search engines and calendar apps use datetime to extract events. Without datetime, the content itself must be a valid date string.
<p>
Published on
<time datetime="2024-07-04">July 4, 2024</time>.
</p>
<p>
Event starts at
<time datetime="2024-07-04T19:00-05:00">7 PM EST</time>.
</p>
<p>
Open
<time datetime="09:00">9:00 AM</time>-
<time datetime="17:00">5:00 PM</time> daily.
</p>mark Element
mark highlights text as relevant for the user's current context, like search-term matches or notes the user added. Unlike strong or em it carries no semantic emphasis—just visual highlighting. The default style is a yellow background. It's also useful inside blockquotes to mark text the quoter wants to draw attention to.
<p>
The function <code>querySelector()</code> returns the
first match, while <mark>querySelectorAll()</mark>
returns a static NodeList.
</p>
<p>
Search results: <mark>HTML5</mark> is the latest
version of the Hypertext Markup Language.
</p>details & summary
details creates a disclosure widget the user can expand or collapse—no JS required. summary is the always-visible label; click it to toggle. Add the open attribute to expand by default. The toggle event fires when it opens or closes. Great for FAQs, advanced settings, and progressive disclosure of content.
<details>
<summary>What is HTML5?</summary>
<p>HTML5 is the fifth major revision of the HTML standard,
introducing semantic elements, new form controls, and APIs
for graphics, media, and offline apps.</p>
</details>
<details open>
<summary>System requirements</summary>
<ul>
<li>Modern browser (Chrome, Firefox, Safari, Edge)</li>
<li>JavaScript enabled</li>
</ul>
</details>dialog Element
dialog represents a modal or non-modal box. show() opens it non-modally; showModal() opens it modally with a backdrop and inert rest of the page. method='dialog' on a form closes the dialog and sets returnValue to the clicked button's value. The close event fires after closing. ::backdrop styles the overlay.
<button onclick="dlg.showModal()">Open dialog</button>
<dialog id="dlg">
<form method="dialog">
<p>Are you sure?</p>
<menu>
<button value="cancel">Cancel</button>
<button value="confirm">OK</button>
</menu>
</form>
</dialog>
<script>
const dlg = document.getElementById('dlg');
dlg.addEventListener('close', () => {
console.log('User chose:', dlg.returnValue);
});
</script>abbr, cite & q
abbr expands abbreviations via title (hover tooltip) and aids screen readers. cite marks the title of a work (book, article, film)—not the author. q is an inline quote (browsers add quotes); blockquote is for block-level. dfn marks the defining instance of a term; subsequent uses can link to it with a.
<p>
<abbr title="World Health Organization">WHO</abbr>
was founded in 1948.
</p>
<p>
As <cite>MDN Web Docs</cite> notes,
<q>HTML is the standard markup language.</q>
</p>
<p>
Use <dfn>semantics</dfn> to describe the meaning of
content, then refer to it normally.
</p>Responsive Design
Meta Viewport
The viewport meta tag tells mobile browsers to use the device width as the layout viewport and to start at scale 1. Without it, phones pretend to be 980px wide. initial-scale=1 prevents zoom on load. viewport-fit=cover lets you use the notch area with env(safe-area-inset-*). Avoid user-scalable=no—it breaks accessibility.
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover">
</head>
<!-- Without this tag, mobile browsers render the page at a
~980px "desktop" width and shrink it down, making text
unreadable. -->
<!-- viewport-fit=cover lets content extend into the notch
area on iPhone X+. -->Picture Element
picture lets you serve different images based on media conditions (art direction). The browser picks the first source whose media matches and ignores the rest. The img is mandatory as a fallback and provides alt text. Use this when you need to crop or change the image entirely per breakpoint, not just resize it.
<picture>
<!-- Art direction: different image per viewport -->
<source media="(min-width: 800px)" srcset="wide.jpg">
<source media="(orientation: portrait)" srcset="tall.jpg">
<img src="default.jpg" alt="A responsive photo">
</picture>
<!-- Browser picks the first matching <source>,
falling back to the <img>. -->srcset & sizes
srcset lists image candidates with their intrinsic width (480w = 480px wide). sizes tells the browser how wide the image will be displayed at each breakpoint. The browser then picks the best candidate based on viewport and device pixel ratio—no JS needed. Use this when the same image just needs different resolutions.
<img src="small.jpg"
srcset="small.jpg 480w,
medium.jpg 800w,
large.jpg 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
alt="A responsive photo">Viewport Units
vw and vh are percentages of the viewport width/height. On mobile, 100vh can include the URL bar, causing jumpiness—prefer dvh (dynamic), svh (small), or lvh (large). clamp(min, preferred, max) is the cleanest way to do fluid typography that doesn't get too small or too large. vmin/vmax use the smaller/larger dimension.
<style>
.hero { height: 100vh; } /* full viewport height */
.half { width: 50vw; } /* half the viewport width */
/* Responsive typography */
h1 {
font-size: clamp(1.5rem, 4vw + 1rem, 3rem);
}
/* Use dvh/svh/lvh to handle mobile browser UI bars */
.hero { height: 100dvh; }
</style>Picture with Type
source with type lets you serve modern image formats with a JPEG/PNG fallback. The browser picks the first type it supports, ignoring the rest. AVIF and WebP offer 30-50% smaller files than JPEG at equal quality. Combined with loading='lazy' on the img, this is the most efficient way to deliver images.
<picture>
<!-- Modern format first, smaller file size -->
<source type="image/avif" srcset="photo.avif">
<source type="image/webp" srcset="photo.webp">
<img src="photo.jpg" alt="Optimized photo" loading="lazy">
</picture>
<!-- Browser picks the first type it supports.
AVIF and WebP are ~30-50% smaller than JPEG/PNG. -->Accessibility (ARIA)
ARIA Roles
ARIA roles tell assistive tech what an element does, but the first rule of ARIA is: don't use it if a native HTML element exists. header, nav, main, article, footer already expose banner, navigation, main, article, contentinfo roles. Use ARIA for custom widgets (tab, dialog, slider, menu) that have no native equivalent, and pair roles with keyboard support.
<div role="banner"> <!-- like <header> -->
<h1>Site title</h1>
</div>
<div role="navigation"> <!-- like <nav> -->
<a href="/">Home</a>
</div>
<div role="main"> <!-- like <main> -->
<div role="article"> <!-- like <article> -->
<p>Content</p>
</div>
</div>
<button role="button" aria-pressed="false">Toggle</button>aria-label & aria-labelledby
aria-label provides an accessible name when no visible text exists (icon-only buttons, etc.). aria-labelledby references the id of visible text to use as the name—useful when a heading already labels a section. Prefer a visible <label for> for form fields. aria-describedby points to hint text read after the label.
<button aria-label="Close menu">×</button>
<nav aria-labelledby="main-nav-heading">
<h2 id="main-nav-heading" class="sr-only">Main navigation</h2>
<ul>...</ul>
</nav>
<input id="email" type="email">
<label id="email-label" for="email">Email address</label>
<!-- aria-labelledby="email-label" would also work -->aria-hidden
aria-hidden='true' removes an element from the accessibility tree—use for purely decorative icons, duplicate text, or off-screen content that shouldn't be announced. Never hide focusable elements. The .sr-only pattern hides text visually while keeping it for screen readers—essential for labels that should be spoken but not seen.
<!-- Decorative icon: hide from screen readers -->
<svg aria-hidden="true" focusable="false">
<use href="#icon-check"></use>
</svg>
<!-- Visually hidden but still announced -->
<span class="sr-only">3 new messages</span>
<style>
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
</style>aria-live Regions
aria-live marks regions whose updates should be announced automatically. polite waits for a pause; assertive interrupts. role='alert' implies assertive and is the standard for error messages. aria-atomic='true' announces the whole region on change; aria-relevant controls which changes are spoken. Use sparingly—overuse makes interfaces noisy.
<!-- Polite: announce when screen reader is idle -->
<div aria-live="polite" id="status">Saving...</div>
<!-- Assertive: announce immediately, may interrupt -->
<div aria-live="assertive" role="alert" id="error"></div>
<script>
function show(msg) {
document.getElementById('status').textContent = msg;
}
function fail(msg) {
document.getElementById('error').textContent = msg;
}
</script>Focus Management
Keyboard users navigate via Tab; focus must always be visible and logical. :focus-visible styles focus only for keyboard, not mouse. When opening a modal, move focus into it; when closing, return focus to the trigger. Trap Tab inside the modal so focus doesn't escape to hidden content. Never set tabindex > 0; use 0 to make elements focusable and -1 to make them programmatically focusable.
<style>
:focus-visible {
outline: 3px solid #3498db;
outline-offset: 2px;
}
</style>
<script>
// Move focus into a modal
function openModal(dlg) {
dlg.showModal();
dlg.querySelector('input, button').focus();
}
// Trap focus inside the modal (simplified)
// Restore focus to the trigger on close
const trigger = document.activeElement;
dlg.addEventListener('close', () => trigger.focus());
</script>Skip Links
A skip link is the first focusable element on the page and lets keyboard users jump past repetitive navigation to the main content. It's visually hidden until focused. Essential for accessibility—WCAG 2.1 Success Criterion 2.4.1. Pair it with a <main id='main'> landmark so the target is clear.
<body>
<a href="#main" class="skip-link">Skip to main content</a>
<header>...nav with 30 links...</header>
<main id="main">...</main>
</body>
<style>
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: #000; color: #fff;
padding: 8px 16px;
z-index: 1000;
}
.skip-link:focus {
left: 0;
}
</style>Web Components
Custom Elements
customElements.define(name, class) registers a custom element. The name must contain a hyphen. Extend HTMLElement for autonomous elements, or a specific class like HTMLButtonElement (with is='name') to extend a built-in. Custom elements are the foundation of web components—reusable, framework-agnostic UI.
<script>
class MyButton extends HTMLElement {
constructor() {
super();
this.addEventListener('click', () => {
console.log('Custom button clicked');
});
}
}
customElements.define('my-button', MyButton);
// Autonomous element: extends HTMLElement
// Customized built-in: extends HTMLButtonElement
// <button is="my-button">
</script>
<my-button>Click me</my-button>Shadow DOM
attachShadow({ mode: 'open' }) creates an encapsulated DOM tree attached to the element. Styles and ids inside the shadow tree don't leak out, and page styles don't leak in (except inheritable properties). :host targets the host element; ::slotted() styles slotted children. mode: 'closed' blocks external access to the shadowRoot—rarely needed.
<script>
class FancyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: block; padding: 16px;
background: #f4f4f4; border-radius: 8px; }
::slotted(*) { color: #333; }
</style>
<div class="card">
<slot></slot>
</div>
`;
}
}
customElements.define('fancy-card', FancyCard);
</script>HTML Templates
The template element holds inert HTML that is parsed but not rendered until cloned. Access its content via template.content, then cloneNode(true) and append. Templates are perfect for repeating structures and form the markup side of web components. Unlike innerHTML, templates can contain <tr>, <td>, <option>, and other elements with parsing quirks.
<template id="row-template">
<tr>
<td class="name"></td>
<td class="age"></td>
</tr>
</template>
<script>
const tpl = document.getElementById('row-template');
const tbody = document.querySelector('tbody');
function addRow(name, age) {
const clone = tpl.content.cloneNode(true);
clone.querySelector('.name').textContent = name;
clone.querySelector('.age').textContent = age;
tbody.appendChild(clone);
}
addRow('Ada', 36);
</script>Slots
Slots let consumers pass light-DOM children into specific places in the shadow tree. A <slot name='x'> receives children with slot='x'; an unnamed <slot> receives the rest. The slotted children stay in the light DOM—the slot is just an insertion point. Listen to slotchange on the slot to react when slotted nodes change.
<fancy-card>
<h2 slot="title">Card Title</h2>
<p>Body text goes here.</p>
</fancy-card>
<!-- Inside the shadow DOM of <fancy-card>: -->
<template>
<div class="card">
<slot name="title"></slot>
<div class="body"><slot></slot></div>
</div>
</template>Lifecycle Callbacks
Custom elements have lifecycle callbacks: connectedCallback (inserted into DOM), disconnectedCallback (removed), attributeChangedCallback (only for attributes listed in observedAttributes), and adoptedCallback (moved to a new document). The constructor runs on creation—keep it light. Do setup in connectedCallback, cleanup in disconnectedCallback. Upgrade happens automatically for elements already in the DOM when define() is called.
<script>
class Observed extends HTMLElement {
static get observedAttributes() {
return ['count', 'label'];
}
constructor() { super(); }
connectedCallback() {
console.log('Added to DOM');
}
disconnectedCallback() {
console.log('Removed from DOM');
}
attributeChangedCallback(name, oldVal, newVal) {
console.log(name, 'changed', oldVal, '->', newVal);
}
}
customElements.define('observed-el', Observed);
</script>Offline Applications
Service Worker Registration
A service worker is a script the browser runs in the background, separate from a page, enabling offline support, push notifications, and background sync. Register it from a page script; the SW file must be served over HTTPS (or localhost) and its scope is its directory by default. Once installed, it controls page loads via fetch events.
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js', { scope: '/' })
.then((reg) => console.log('SW registered', reg.scope))
.catch((err) => console.error('SW failed', err));
});
}
</script>Cache API
The Cache API stores Request/Response pairs, persisted across sessions. In the install event, pre-cache critical assets. In the fetch event, respond from cache first and fall back to the network (cache-first strategy). Other strategies: network-first, stale-while-revalidate, cache-only, network-only. Bump the cache name to invalidate old entries.
// sw.js
const CACHE = 'app-v1';
const ASSETS = ['/', '/styles.css', '/app.js', '/offline.html'];
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE).then((c) => c.addAll(ASSETS))
);
});
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then((r) => r || fetch(e.request))
);
});Offline Fallback
A network-first strategy with an offline fallback tries the network, then cache, then a generic offline page. The activate event runs after install completes and is the right place to delete old caches—otherwise they pile up. Clients can claim existing tabs immediately with self.clients.claim() inside activate.
// sw.js
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request).catch(() =>
caches.match(e.request).then((r) =>
r || caches.match('/offline.html')
)
)
);
});
// Activate event: clean up old caches
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((k) => k !== 'app-v1')
.map((k) => caches.delete(k))
)
)
);
});Web App Manifest
A web app manifest is a JSON file describing how the app should look when installed on the home screen. name/short_name, icons, start_url, and display (standalone, fullscreen, minimal-ui, browser) are the key fields. Combined with a service worker, a manifest makes the app installable (PWA). theme_color affects the OS UI chrome.
<!-- index.html -->
<link rel="manifest" href="manifest.json">
<!-- manifest.json -->
{
"name": "My App",
"short_name": "App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3498db",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}Background Sync
Background Sync (registered via navigator.serviceWorker.ready.then(r => r.sync.register('tag'))) defers work until the user has connectivity. The browser fires a sync event in the service worker when network returns—even if the page is closed. Use it to send queued form submissions, messages, or analytics. tag identifies the sync; duplicates are coalesced.
// sw.js
self.addEventListener('sync', (e) => {
if (e.tag === 'send-messages') {
e.waitUntil(sendQueuedMessages());
}
});
async function sendQueuedMessages() {
const queue = await getQueue();
for (const msg of queue) {
try { await fetch('/api/send', { method: 'POST', body: msg }); }
catch (e) { throw e; } // retry later
}
}SEO Best Practices
Title & Meta Description
The title (under ~60 chars) is the most important on-page SEO signal and appears as the search result's headline. The meta description (under ~160 chars) is the snippet text—it doesn't affect ranking but affects click-through. Use one title per page, include the primary keyword, and make it compelling. canonical prevents duplicate-content issues.
<head>
<title>HTML5 Cheatsheet - Semantic Elements & APIs</title>
<meta name="description"
content="A quick reference to HTML5 semantic elements,
form types, and browser APIs with examples.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://example.com/html5">
</head>Open Graph & Twitter Cards
Open Graph tags control how your page looks when shared on Facebook, LinkedIn, Slack, and most social platforms. og:image should be 1200x630px. Twitter Cards use a similar set with twitter: prefixes (or fall back to OG). Without these, shares get a random or no preview image, dramatically reducing click-through.
<head>
<!-- Open Graph (Facebook, LinkedIn, etc.) -->
<meta property="og:title" content="HTML5 Cheatsheet">
<meta property="og:description" content="Quick HTML5 reference.">
<meta property="og:image" content="https://example.com/og.png">
<meta property="og:url" content="https://example.com/html5">
<meta property="og:type" content="article">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@example">
</head>Canonical & hreflang
rel='canonical' tells search engines which URL is the master version when several URLs have similar content (print, sort, query params)—prevents duplicate-content penalties. hreflang points to translated or regional versions so Google serves the right one. Always include an x-default for unmatched locales. Both must be absolute URLs.
<head>
<!-- Canonical: the master URL for duplicate content -->
<link rel="canonical" href="https://example.com/article">
<!-- hreflang: language/region alternatives -->
<link rel="alternate" hreflang="en" href="https://example.com/en/article">
<link rel="alternate" hreflang="es" href="https://example.com/es/article">
<link rel="alternate" hreflang="x-default" href="https://example.com/article">
</head>Structured Data (JSON-LD)
JSON-LD structured data describes your page in a machine-readable format (schema.org vocabulary) and enables rich results in search: article carousels, breadcrumbs, FAQs, reviews, events, products. Put it in a <script type='application/ld+json'>. Validate with Google's Rich Results Test. It doesn't directly boost rankings but improves how results look.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "HTML5 Cheatsheet",
"author": { "@type": "Person", "name": "Ada" },
"datePublished": "2024-07-04",
"image": "https://example.com/cover.png",
"publisher": {
"@type": "Organization",
"name": "Example"
}
}
</script>Semantic Headings
Search engines and screen readers use headings to understand the page outline. Use one h1 (the page title), then h2 for major sections, h3 for subsections—never skip levels (no h2 straight to h4). Don't choose a heading for its size; use CSS for styling. A logical heading hierarchy improves both SEO and accessibility.
<article>
<h1>Article Title (only one h1 per page)</h1>
<p>Intro paragraph.</p>
<section>
<h2>First major section</h2>
<p>Content...</p>
<h3>Subsection</h3>
<p>Detail...</p>
</section>
<section>
<h2>Second major section</h2>
<p>Content...</p>
</section>
</article>Performance Optimization
preload
rel='preload' tells the browser to fetch a high-priority resource the current page will need, before it would normally be discovered. Always specify as (font, style, script, image, fetch, etc.) so the browser applies correct priorities and CSP. Fonts need crossorigin. Use preload only for truly critical resources—overuse wastes bandwidth.
<head>
<!-- Preload critical resources for the current page -->
<link rel="preload" href="/fonts/main.woff2"
as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/app.js" as="script">
<!-- Preload a hero image -->
<link rel="preload" href="/hero.webp" as="image">
</head>prefetch & preconnect
preconnect completes DNS, TCP, and TLS to an origin early, saving ~100-300ms when the actual request happens—use for critical third-party domains. dns-prefetch is the lighter version (DNS only). prefetch fetches a low-priority resource for a likely next navigation. Use them sparingly: too many preconnects waste sockets, and prefetched resources may go unused.
<head>
<!-- preconnect: early handshake to a third-party origin -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="preconnect" href="https://api.example.com" crossorigin>
<!-- dns-prefetch: just DNS lookup, lighter -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<!-- prefetch: fetch a resource for the NEXT navigation -->
<link rel="prefetch" href="/next-page.html">
</head>lazy loading
loading='lazy' on img and iframe defers loading until the element approaches the viewport—saving bandwidth and CPU on initial render. decoding='async' prevents image decode from blocking the main thread. Always set width/height to avoid layout shift. Use loading='eager' (default) and fetchpriority='high' only for above-the-fold hero images. Supported in all modern browsers.
<!-- Native lazy loading (no JS needed) -->
<img src="photo.jpg" loading="lazy" decoding="async" alt="...">
<iframe src="embed.html" loading="lazy"></iframe>
<!-- Eager (default) loads immediately -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero">
<!-- Defer non-critical images until they near the viewport -->
<img src="below-fold.jpg" loading="lazy" width="800" height="600" alt="">defer & async
A plain <script> blocks HTML parsing while it downloads and runs. async downloads in parallel and runs as soon as ready—use for independent scripts (analytics, ads) where order doesn't matter. defer downloads in parallel but waits to run, in order, after the document is parsed—use for main app scripts that depend on the DOM. defer scripts run before DOMContentLoaded.
<head>
<!-- async: download in parallel, run ASAP (order not guaranteed) -->
<script src="analytics.js" async></script>
<!-- defer: download in parallel, run after parse, in order -->
<script src="app.js" defer></script>
<script src="page.js" defer></script>
<!-- Classic: block parsing to download and run -->
<script src="blocking.js"></script>
</head>Resource Hints Summary
preload (current page), prefetch (next page), preconnect (origin handshake), and dns-prefetch (DNS only) are the core resource hints. prerender is deprecated in favor of the Speculation Rules API, which can fully prerender pages for instant navigation. Apply hints based on real user behavior—over-hinting wastes data and can hurt performance.
<head>
<link rel="preload" href="/critical.woff2" as="font" crossorigin>
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="prefetch" href="/next-page.html">
<!-- Speculation Rules API (Chrome) -->
<script type="speculationrules">
{ "prerender": [{ "where": { "href_matches": "/next/*" } }] }
</script>
</head>Critical Rendering
Inlining critical CSS for above-the-fold content removes the render-blocking request and improves First Contentful Paint. Load the rest via preload+swap. content-visibility: auto lets the browser skip rendering off-screen sections, dramatically improving scroll performance for long pages—pair with contain-intrinsic-size to reserve space and avoid scrollbar jumpiness.
<head>
<!-- Inline critical CSS for above-the-fold -->
<style>
body { margin: 0; font: system-ui; }
.hero { height: 60vh; background: #3498db; }
</style>
<!-- Load the rest asynchronously -->
<link rel="preload" href="/full.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/full.css"></noscript>
</head>
<body>
<!-- Use content-visibility to skip off-screen rendering -->
<section style="content-visibility: auto; contain-intrinsic-size: 500px;">
Heavy content here
</section>
</body>Fragmentos de HTML5 relacionados
Copy-paste ready code for common tasks.
Semantic Elements
Structure a page with header, nav, main, article, and footer.
Form Input Types
Use native HTML5 input types, validation, and datalist.
Canvas Basics
Draw shapes, lines, and text on a 2D canvas.
Video & Audio
Embed media with multiple sources and control playback.
Local & Session Storage
Persist data in the browser with the Web Storage API.
Geolocation
Get the user's position and watch for changes.
Drag and Drop
Accept dropped files with the Drag and Drop API.
Web Workers
Run heavy computation on a background thread.
Was this helpful?