All topics
Frontend · Learning hub

HTML notes for developers

Master HTML with a curated set of 12 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
HTML

HTML Structure & Semantics

HTML Structure & Semantics Semantic HTML uses elements that carry meaning about the content they contain - not just how it looks. This improves accessibility, S

HTML Structure & Semantics

Semantic HTML uses elements that carry meaning about the content they contain - not just how it looks. This improves accessibility, SEO, and code maintainability. Screen readers, search engines, and browser reading modes all rely on semantic structure.

Document Boilerplate

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- Character encoding - always first -->
  <meta charset="UTF-8">

  <!-- Responsive viewport - essential for mobile -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- Page title (shown in tab, used by search engines) -->
  <title>Page Title | Site Name</title>

  <!-- SEO meta tags -->
  <meta name="description" content="150-160 character page description for search results">
  <meta name="robots" content="index, follow">
  <link rel="canonical" href="https://example.com/page">

  <!-- Open Graph (social sharing) -->
  <meta property="og:title" content="Page Title">
  <meta property="og:description" content="Description for social cards">
  <meta property="og:image" content="https://example.com/og-image.jpg">
  <meta property="og:url" content="https://example.com/page">
  <meta property="og:type" content="website">

  <!-- Twitter Card -->
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:site" content="@yourhandle">

  <!-- Favicon variants -->
  <link rel="icon" href="/favicon.ico" sizes="32x32">
  <link rel="icon" href="/icon.svg" type="image/svg+xml">
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">

  <!-- Performance: preload critical resources -->
  <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preconnect" href="https://fonts.googleapis.com">

  <!-- Stylesheets -->
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <!-- page content -->
  <script src="/app.js" defer></script>
</body>
</html>

Semantic Page Structure

Use landmark elements to give the page a clear, machine-readable structure. Each landmark maps to an ARIA role that assistive technology uses for navigation.

<body>
  <!-- Site-wide header with branding and nav -->
  <header>
    <a href="/" aria-label="Home"><img src="/logo.svg" alt="Company Name"></a>
    <nav aria-label="Main navigation">
      <ul>
        <li><a href="/about" aria-current="page">About</a></li>
        <li><a href="/blog">Blog</a></li>
        <li><a href="/contact">Contact</a></li>
      </ul>
    </nav>
  </header>

  <!-- Main content - only one per page -->
  <main id="main-content">   <!-- Skip-link target -->

    <!-- Standalone, syndicate-able piece of content -->
    <article>
      <header>
        <h1>Article Title</h1>
        <p>By <address><a href="/author/jane">Jane Doe</a></address>
           on <time datetime="2025-03-15">March 15, 2025</time></p>
      </header>

      <!-- Related content within an article -->
      <section aria-labelledby="intro-heading">
        <h2 id="intro-heading">Introduction</h2>
        <p>Content...</p>
      </section>

      <section aria-labelledby="details-heading">
        <h2 id="details-heading">Details</h2>
        <p>Content...</p>
      </section>

      <footer>
        <p>Filed under: <a href="/tags/html">HTML</a></p>
      </footer>
    </article>

    <!-- Tangentially related content -->
    <aside aria-label="Related articles">
      <h2>You might also like</h2>
      <!-- ... -->
    </aside>
  </main>

  <!-- Site-wide footer -->
  <footer>
    <p>&copy; 2025 Company Name. <a href="/privacy">Privacy Policy</a></p>
  </footer>
</body>

Semantic Text Elements

<!-- Headings: only one h1, use logical hierarchy -->
<h1>Page title (one per page)</h1>
<h2>Section heading</h2>
<h3>Subsection</h3>

<!-- Emphasis - semantic, not just visual -->
<p>Press <strong>Ctrl+S</strong> to save.</p>          <!-- strong importance -->
<p>The word <em>semantics</em> is key here.</p>       <!-- stress emphasis -->
<p>Total: <b>$42.00</b></p>                           <!-- visually bold, no semantic weight -->
<p>The book is called <i>Clean Code</i></p>           <!-- alternative voice/mood -->

<!-- Inline code & technical text -->
<p>Use <code>npm install</code> to get started.</p>
<p>Keyboard shortcut: <kbd>Cmd</kbd> + <kbd>K</kbd></p>
<p>The variable was <var>x</var></p>
<pre><code class="language-js">const x = 42;</code></pre>

<!-- Quotes -->
<blockquote cite="https://source.example">
  <p>The best code is no code at all.</p>
  <footer>— <cite>Jeff Atwood</cite></footer>
</blockquote>
<p>He said <q>hello</q> and left.</p>

<!-- Other semantic inline elements -->
<p>The price is <del>$99</del> <ins>$49</ins> today.</p>
<p>Water is H<sub>2</sub>O. E = mc<sup>2</sup>.</p>
<p>Result: <mark>critically important text</mark></p>
<p>The meeting is at <time datetime="2025-03-15T14:00">2pm</time></p>
<abbr title="Hypertext Markup Language">HTML</abbr>

Lists, Tables & Media

<!-- Description list (key-value pairs) -->
<dl>
  <dt>HTTP</dt>
  <dd>Hypertext Transfer Protocol</dd>
  <dt>REST</dt>
  <dd>Representational State Transfer</dd>
</dl>

<!-- Accessible table -->
<table>
  <caption>Monthly Sales Report</caption>
  <thead>
    <tr>
      <th scope="col">Month</th>
      <th scope="col">Revenue</th>
      <th scope="col">Users</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">January</th>
      <td>$12,400</td>
      <td>842</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">Total</th>
      <td>$12,400</td>
      <td>842</td>
    </tr>
  </tfoot>
</table>

<!-- Responsive images -->
<picture>
  <source media="(min-width: 1200px)" srcset="/hero-large.webp" type="image/webp">
  <source media="(min-width: 600px)" srcset="/hero-medium.webp" type="image/webp">
  <img src="/hero-small.jpg" alt="Developer working on laptop" width="800" height="400"
       loading="lazy" decoding="async">
</picture>

<!-- Embedded video -->
<video controls preload="metadata" poster="/thumbnail.jpg">
  <source src="/video.webm" type="video/webm">
  <source src="/video.mp4" type="video/mp4">
  <track kind="subtitles" src="/captions.vtt" srclang="en" label="English" default>
  <p>Your browser does not support video. <a href="/video.mp4">Download</a></p>
</video>
HTML

Forms & Input Types

Forms & Input Types HTML5 forms have powerful built-in validation, diverse input types, and accessibility features. Using the right input type gives you free mo

Forms & Input Types

HTML5 forms have powerful built-in validation, diverse input types, and accessibility features. Using the right input type gives you free mobile keyboard optimization, native UX, and client-side validation without JavaScript.

Form Structure & Attributes

<form
  action="/api/register"
  method="POST"
  enctype="multipart/form-data"   <!-- Required for file uploads -->
  novalidate                        <!-- Disable browser validation (use your own) -->
  autocomplete="on"
  aria-label="User registration form"
>
  <!-- fieldset groups related inputs; legend labels the group -->
  <fieldset>
    <legend>Personal Information</legend>

    <!-- label must reference input via for/id OR wrap the input -->
    <label for="full-name">Full Name <span aria-hidden="true">*</span></label>
    <input
      type="text"
      id="full-name"
      name="fullName"
      required
      minlength="2"
      maxlength="100"
      autocomplete="name"
      placeholder="Jane Doe"
      aria-required="true"
      aria-describedby="name-hint name-error"
    >
    <span id="name-hint" class="hint">First and last name</span>
    <span id="name-error" role="alert" class="error" hidden>Name is required</span>
  </fieldset>

  <!-- Submit button -->
  <button type="submit">Create Account</button>
  <button type="reset">Clear Form</button>
  <button type="button" id="preview-btn">Preview</button>
</form>

All Input Types

<!-- Text inputs -->
<input type="text"     name="username">            <!-- Plain text -->
<input type="email"    name="email" multiple>      <!-- Validates email format; "email" keyboard on mobile -->
<input type="password" name="pwd" autocomplete="new-password">
<input type="search"   name="q">                  <!-- With clear button on some browsers -->
<input type="url"      name="website">             <!-- Validates URL format -->
<input type="tel"      name="phone" pattern="[0-9]{10}">  <!-- Numeric keyboard on mobile -->

<!-- Numeric -->
<input type="number"   name="qty"   min="1" max="100" step="1">
<input type="range"    name="vol"   min="0" max="100" step="5" value="50">

<!-- Date/Time -->
<input type="date"           name="birthday"  min="1900-01-01" max="2025-12-31">
<input type="time"           name="start"     min="09:00" max="17:00" step="900">  <!-- 15min steps -->
<input type="datetime-local" name="event">    <!-- Date + time, no timezone -->
<input type="month"          name="exp">      <!-- YYYY-MM -->
<input type="week"           name="week">     <!-- YYYY-W## -->

<!-- Selectors -->
<input type="color"    name="brand-color" value="#ff6600">
<input type="file"     name="avatar" accept="image/png,image/jpeg" multiple>

<!-- Checkboxes & Radios -->
<input type="checkbox" name="agree"   id="agree"   required>
<input type="radio"    name="plan"    value="free"  checked>
<input type="radio"    name="plan"    value="pro">

<!-- Hidden & Submit -->
<input type="hidden"   name="csrf_token" value="abc123">
<input type="submit"   value="Submit">
<input type="image"    src="/btn.png" alt="Submit order">  <!-- Image submit button -->
<input type="reset">

Validation Attributes

<!-- Built-in validation - no JS needed -->
<input
  type="email"
  required                              <!-- Must not be empty -->
  minlength="5"                         <!-- Min character count -->
  maxlength="254"                       <!-- Max character count -->
  pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"  <!-- Regex pattern -->
  title="Please enter a valid email address"  <!-- Shown in browser tooltip on invalid -->
>

<input type="number" min="0" max="999" step="0.01">  <!-- Numeric range + increment -->
<input type="file" accept=".pdf,.doc,.docx" multiple> <!-- File type filter -->
<input type="url" pattern="https://.*">              <!-- HTTPS only -->

<!-- Custom validation message via JS -->
<input type="email" id="email" oninvalid="this.setCustomValidity('Enter a valid email')" oninput="this.setCustomValidity('')">

<!-- Constraint Validation API -->
<script>
  const form = document.querySelector('form');
  form.addEventListener('submit', e => {
    if (!form.checkValidity()) {
      e.preventDefault();
      // reportValidity() shows browser tooltips
      form.reportValidity();
    }
  });

  // Check individual input
  const email = document.getElementById('email');
  console.log(email.validity.typeMismatch);  // true if wrong type
  console.log(email.validity.valueMissing);  // true if required + empty
  console.log(email.validationMessage);      // browser message string
</script>

Select, Textarea, Datalist

<!-- Select dropdown -->
<label for="country">Country</label>
<select id="country" name="country" required autocomplete="country">
  <option value="">-- Select a country --</option>
  <optgroup label="North America">
    <option value="US">United States</option>
    <option value="CA">Canada</option>
    <option value="MX">Mexico</option>
  </optgroup>
  <optgroup label="Europe">
    <option value="GB" selected>United Kingdom</option>
    <option value="DE">Germany</option>
  </optgroup>
</select>

<!-- Multiple select -->
<select name="skills" multiple size="5">
  <option value="js">JavaScript</option>
  <option value="ts">TypeScript</option>
  <option value="react">React</option>
</select>

<!-- Textarea -->
<label for="bio">Bio</label>
<textarea
  id="bio"
  name="bio"
  rows="4"
  cols="50"
  minlength="20"
  maxlength="500"
  placeholder="Tell us about yourself..."
  autocomplete="off"
  spellcheck="true"
></textarea>  <!-- Note: no space between tags - whitespace is content -->

<!-- Datalist: free-text input with suggestions (not a constraint) -->
<label for="framework">Framework</label>
<input list="frameworks" id="framework" name="framework">
<datalist id="frameworks">
  <option value="React">
  <option value="Vue">
  <option value="Angular">
  <option value="Svelte">
  <option value="Next.js">
</datalist>
HTML

Accessibility (a11y)

HTML Accessibility (a11y) Accessibility means building for everyone - including users with visual, motor, auditory, and cognitive disabilities. Most a11y issues

HTML Accessibility (a11y)

Accessibility means building for everyone - including users with visual, motor, auditory, and cognitive disabilities. Most a11y issues are fixable with correct HTML; ARIA is the last resort, not the first tool.

ARIA Roles & Attributes

ARIA (Accessible Rich Internet Applications) fills gaps where HTML semantics are insufficient. The first rule of ARIA: don't use ARIA - use native HTML elements instead whenever possible.

<!-- Role: overrides or provides semantic meaning -->
<div role="button" tabindex="0">Custom Button</div>  <!-- Prefer <button> -->
<div role="alert">Error: Form submission failed</div>  <!-- Live region, announced immediately -->
<div role="status">Changes saved</div>                 <!-- Polite live region -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm Delete</h2>
  <p>Are you sure you want to delete this item?</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

<!-- aria-label: names an element without visible text -->
<button aria-label="Close dialog">
  <svg aria-hidden="true"><use href="#icon-x"/></svg>
</button>
<nav aria-label="Breadcrumb">...</nav>         <!-- Distinguish multiple navs -->
<nav aria-label="Pagination">...</nav>

<!-- aria-labelledby: points to existing visible text -->
<section aria-labelledby="products-heading">
  <h2 id="products-heading">Featured Products</h2>
</section>

<!-- aria-describedby: provides additional description -->
<input type="password" aria-describedby="pwd-hint">
<span id="pwd-hint">Min 8 characters, include a number</span>

<!-- State attributes -->
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>...</ul>

<input type="checkbox" aria-checked="mixed">  <!-- Indeterminate state -->
<li role="option" aria-selected="true">Option 1</li>
<button aria-pressed="true">Bold</button>     <!-- Toggle button -->
<span aria-live="polite" aria-atomic="true">Loading...</span>

<!-- aria-hidden: hide decorative elements from screen readers -->
<span aria-hidden="true">★★★☆☆</span>
<span class="sr-only">3 out of 5 stars</span>  <!-- Visible only to screen readers -->

<!-- Disable an element for AT -->
<div aria-disabled="true">Unavailable option</div>

Skip Links & Keyboard Navigation

<!-- Skip link: first focusable element, bypasses repetitive nav -->
<!-- Visible on focus, hidden otherwise -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>...</header>
<main id="main-content" tabindex="-1">...</main>

<!-- CSS for skip link (show on focus only) -->
<style>
.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px 16px;
  z-index: 9999;
  text-decoration: none;
}
.skip-link:focus {
  top: 0;
}
</style>

<!-- tabindex -->
<div tabindex="0">Focusable div (avoid - use <button> or <a>)</div>
<div tabindex="-1">Programmatically focusable, not in tab order</div>
<!-- tabindex="1+" disrupts natural order - AVOID -->

<!-- Focus management in modals -->
<script>
function openModal(modalEl) {
  modalEl.removeAttribute('hidden');
  // Move focus into modal
  const firstFocusable = modalEl.querySelector(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  firstFocusable?.focus();
  // Trap focus inside modal
  modalEl.addEventListener('keydown', trapFocus);
}
function closeModal(modalEl, triggerEl) {
  modalEl.setAttribute('hidden', '');
  // Return focus to the element that opened the modal
  triggerEl?.focus();
}
</script>

Images, Color & Screen Readers

<!-- Alt text guidelines -->
<img src="hero.jpg" alt="Developer typing on a mechanical keyboard at night">
<!-- Informative image: describe what it shows, why it's there -->

<img src="logo.svg" alt="Acme Corp">
<!-- Logo: company name only -->

<img src="decorative-border.png" alt="">
<!-- Decorative: empty alt, screen reader skips it -->

<img src="chart.png"
  alt="Bar chart showing 40% increase in users from Q1 to Q2 2025"
  aria-describedby="chart-details">
<p id="chart-details">Full data table: Q1: 1,200 users. Q2: 1,680 users. Growth: 480 (40%).</p>
<!-- Complex image: short alt + long description -->

<!-- Icon buttons: always label the action -->
<button aria-label="Delete item">
  <svg aria-hidden="true" focusable="false">
    <use href="#trash-icon"></use>
  </svg>
</button>

<!-- Color contrast requirements (WCAG 2.1 AA) -->
<!-- Normal text (< 18pt): 4.5:1 ratio -->
<!-- Large text (>= 18pt or 14pt bold): 3:1 ratio -->
<!-- UI components, icons: 3:1 ratio against adjacent colors -->
<!-- Never use color alone to convey information -->
<p style="color: red;">Error</p>  <!-- Bad: color only -->
<p>
  <span aria-hidden="true">⚠ </span>
  <strong style="color: red;">Error:</strong> Name is required.
</p>  <!-- Good: icon + bold + color -->

<!-- Screen reader utility class -->
<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>
<span class="sr-only">Loading, please wait...</span>

Accessible Forms & Error Handling

<!-- Every input MUST have a label -->
<!-- Method 1: for + id -->
<label for="email">Email address</label>
<input type="email" id="email" name="email">

<!-- Method 2: wrapping label -->
<label>
  Email address
  <input type="email" name="email">
</label>

<!-- Method 3: aria-label (last resort, not visible) -->
<input type="search" aria-label="Search products" name="q">

<!-- Required field indication -->
<label for="name">
  Full Name
  <span aria-hidden="true"> *</span>  <!-- Asterisk for sighted users -->
</label>
<input type="text" id="name" required aria-required="true">
<p id="required-note">Fields marked <span aria-hidden="true">*</span> are required</p>

<!-- Accessible error messages -->
<input
  type="email"
  id="email"
  aria-invalid="true"
  aria-describedby="email-error"
  aria-required="true"
>
<!-- role="alert" announces immediately when inserted into DOM -->
<p id="email-error" role="alert" style="color: red;">
  <span aria-hidden="true">✕ </span>
  Please enter a valid email address.
</p>

<!-- Success feedback -->
<div role="status" aria-live="polite">
  Your changes have been saved.
</div>
HTML

HTML Interview Questions

HTML Interview Questions Core HTML questions that come up in frontend, fullstack, and web developer interviews. Many candidates underestimate HTML knowledge - s

HTML Interview Questions

Core HTML questions that come up in frontend, fullstack, and web developer interviews. Many candidates underestimate HTML knowledge - strong answers here set you apart.

1. What is semantic HTML and why does it matter?

Semantic HTML uses elements whose names describe their meaning and purpose, not just their visual appearance. Examples: <article> for standalone content, <nav> for navigation, <button> for interactive controls vs a styled <div>. It matters for three reasons: (1) Accessibility - screen readers use semantic structure to let users navigate by landmarks, headings, and form fields. (2) SEO - search engines weight content inside <main>, <article>, <h1> higher than generic divs. (3) Maintainability - code communicates intent. The rule: choose the element with the correct semantic meaning; use CSS to change how it looks.

2. What are data- attributes and when would you use them?

data-* attributes store custom data on HTML elements without using non-standard attributes or making extra server requests. They can hold any string value and are accessible via element.dataset in JavaScript. Example: <button data-product-id="42" data-action="add-to-cart">. Access via btn.dataset.productId (camelCase). Use cases: passing server-rendered data to JavaScript, tracking analytics metadata, storing state for CSS styling via attribute selectors ([data-state="active"]), and configuration options for web components. Avoid storing sensitive data in them since they are visible in the DOM.

3. What is the difference between defer and async on script tags?

Both attributes make script loading non-blocking (download happens in parallel with HTML parsing), but they differ in execution timing. async: script executes as soon as it downloads, potentially before the DOM is fully parsed - order is not guaranteed. Best for independent scripts like analytics. defer: script executes after the HTML is fully parsed, in document order. Best for scripts that interact with the DOM or depend on each other. A plain <script> tag (neither) blocks HTML parsing during download and execution. Rule of thumb: use defer for most scripts; use async only for truly independent analytics/tracking scripts.

4. How does srcset work and when would you use the picture element?

srcset on <img> provides multiple image versions at different resolutions; the browser picks the best one based on device pixel ratio and viewport width. Example: srcset="/img-400.jpg 400w, /img-800.jpg 800w" sizes="(max-width: 600px) 100vw, 50vw". The browser uses the sizes hint to calculate actual display size, then picks the most efficient resolution. The <picture> element goes further - it allows art direction (completely different crops for different screens) using <source media="..."> and different format negotiation (WebP with JPEG fallback) using type="image/webp". Use srcset+sizes for resolution switching; use <picture> for art direction or format fallback.

5. What is the purpose of the meta viewport tag?

Without the viewport meta tag, mobile browsers render pages at a virtual ~980px wide viewport (to avoid breaking desktop-only sites) then scale down, making text tiny. The tag <meta name="viewport" content="width=device-width, initial-scale=1.0"> tells the browser to set the viewport width equal to the device's actual screen width and use 1:1 pixel scale. This is the prerequisite for responsive design - CSS media queries check the actual viewport width, not the virtual one. Never use maximum-scale=1.0, user-scalable=no because they disable browser zoom, creating a WCAG accessibility violation.

6. What is the difference between preload, prefetch, and preconnect?

These are resource hints that control browser prioritization. preload (<link rel="preload" as="font">): fetch this resource immediately with high priority because it is needed for the current page - for critical above-the-fold resources. prefetch (<link rel="prefetch">): low priority fetch for resources needed on the next navigation (next page, next step in a flow). preconnect (<link rel="preconnect" href="https://fonts.googleapis.com">): establish the TCP connection, TLS handshake, and DNS lookup for a third-party origin in advance - saves ~100-500ms when the actual request fires. Use preconnect for critical third-party origins; preload for render-blocking assets like custom fonts.

7. What is ARIA and when should you use it?

ARIA (Accessible Rich Internet Applications) is a set of HTML attributes (roles, states, properties) that describe the semantics of custom UI components to assistive technology. The first rule of ARIA is: do not use ARIA - prefer native HTML semantics. <button> is better than <div role="button"> because you get keyboard focus, Enter/Space activation, and semantics for free. ARIA is appropriate when: building custom widgets that have no HTML equivalent (tabs, tree views, comboboxes, sliders), updating dynamic content (aria-live regions for notifications), and bridging gaps in framework-rendered SPAs where semantic structure is lost. Never use ARIA to fix broken HTML - fix the HTML instead.

8. What is the difference between block, inline, and inline-block elements?

Block-level elements (<div>, <p>, <section>, <h1>-<h6>, <ul>, <li>, <table>) start on a new line and stretch to full container width by default - you can set width and height. Inline elements (<span>, <a>, <strong>, <em>, <img>) flow within text, only take up as much space as their content, and width/height properties are ignored (for replaced elements like img, they work). Inline-block combines both: flows inline with text but respects width, height, margin, and padding. Note: display is a CSS property - HTML elements have default display values that can be changed with CSS.

9. What is the difference between id and class attributes?

id must be unique within a page - only one element can have a given id. It is used for: fragment links (<a href="#section1">), form label associations (for/id pairing), JavaScript getElementById(), and CSS targeting (though high specificity makes this fragile). class can be applied to multiple elements and one element can have multiple classes. Classes are the primary styling hook. For JavaScript, prefer data attributes over id for behavior hooks. For accessibility, id is required for aria-labelledby, aria-describedby, and aria-controls associations. Duplicate ids are a common bug that breaks these ARIA relationships.

10. How does HTML handle character encoding and why does it matter?

Character encoding maps byte sequences to characters. UTF-8 encodes all Unicode characters and should always be used. The <meta charset="UTF-8"> tag must be the first element in <head> (within the first 1024 bytes) so the browser knows how to parse the rest of the document. Without it, browsers fall back to a detected or default encoding (often Windows-1252 or Latin-1) which corrupts non-ASCII characters like accented letters, emoji, and non-Latin scripts. The HTTP Content-Type: text/html; charset=UTF-8 header takes precedence over the meta tag. Both should match. HTML entities like &amp;, &lt;, &gt;, &quot; are alternatives for characters with special HTML meaning and do not depend on encoding.

HTML

Document Structure & Metadata

HTML: Document Structure & Metadata Every HTML document has a consistent structure. The <head> is for metadata and resources; the <body> is for visible content.

HTML: Document Structure & Metadata

Every HTML document has a consistent structure. The <head> is for metadata and resources; the <body> is for visible content.

Document Boilerplate

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Page description for SEO, max 155 chars">

  <title>Page Title — Site Name</title>

  <!-- Canonical URL (prevent duplicate content) -->
  <link rel="canonical" href="https://example.com/page">

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

  <!-- CSS -->
  <link rel="stylesheet" href="/styles.css">

  <!-- Preconnect to external origins (speeds up DNS + TLS) -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

  <!-- Preload critical resources -->
  <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/hero.webp" as="image">
</head>
<body>
  <!-- content -->
  <script src="/app.js" defer></script>
</body>
</html>

Key Meta Tags

<!-- Character encoding — always UTF-8, always first in <head> -->
<meta charset="UTF-8">

<!-- Viewport — essential for responsive design -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- On iOS, prevent font size adjustment in landscape -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">

<!-- SEO -->
<meta name="description" content="Clear description, 120-155 characters">
<meta name="robots" content="index, follow">
<meta name="robots" content="noindex, nofollow">  <!-- prevent indexing -->

<!-- Theme color (browser chrome on mobile) -->
<meta name="theme-color" content="#0070f3">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">

<!-- Referrer policy -->
<meta name="referrer" content="strict-origin-when-cross-origin">

Open Graph & Social Meta Tags

<!-- Open Graph — controls how page looks when shared on Facebook, LinkedIn, etc. -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/page">
<meta property="og:title" content="Page Title">
<meta property="og:description" content="Description shown in the link preview">
<meta property="og:image" content="https://example.com/og-image.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:site_name" content="My Site">

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@mysite">
<meta name="twitter:creator" content="@author">
<meta name="twitter:title" content="Page Title">
<meta name="twitter:description" content="Description">
<meta name="twitter:image" content="https://example.com/twitter-image.jpg">

Script Loading Strategies

<!-- Blocking — stops HTML parsing until script is downloaded and executed (avoid) -->
<script src="app.js"></script>

<!-- defer — downloads in parallel, executes AFTER HTML parsing completes, in order -->
<script src="app.js" defer></script>

<!-- async — downloads in parallel, executes IMMEDIATELY when ready (ignores order) -->
<script src="analytics.js" async></script>

<!-- type="module" — deferred by default, own scope, supports import -->
<script type="module" src="app.mjs"></script>

<!-- Inline script — executes immediately -->
<script>
  document.getElementById('app').style.display = 'block';
</script>

<!-- Best practice: put scripts before </body> OR use defer for head scripts -->

Link Relationships

<!-- Stylesheet -->
<link rel="stylesheet" href="styles.css" media="print">   <!-- only for print -->
<link rel="stylesheet" href="dark.css" media="(prefers-color-scheme: dark)">

<!-- Alternate versions -->
<link rel="alternate" hreflang="uk" href="https://example.com/uk/page">
<link rel="alternate" hreflang="x-default" href="https://example.com/page">

<!-- RSS feed -->
<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="Blog RSS">

<!-- DNS prefetch (cheap — just DNS lookup) -->
<link rel="dns-prefetch" href="https://api.example.com">

<!-- Preconnect (DNS + TCP + TLS — more expensive, use sparingly) -->
<link rel="preconnect" href="https://fonts.googleapis.com">

<!-- Prefetch next page (low priority background fetch) -->
<link rel="prefetch" href="/next-page.html">

<!-- Prerender next page (render in background, instant navigation) -->
<link rel="prerender" href="/next-page.html">
HTML

Semantic HTML5 Elements

Semantic HTML5 Elements Semantic HTML uses meaningful element names that describe the content's purpose. It improves accessibility, SEO, maintainability, and he

Semantic HTML5 Elements

Semantic HTML uses meaningful element names that describe the content's purpose. It improves accessibility, SEO, maintainability, and helps screen readers and search engines understand your content.

Document Structure Elements

<body>
  <header>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <article>
      <header>
        <h1>Article Title</h1>
        <time datetime="2024-01-15">January 15, 2024</time>
        <address>By <a href="/author/alice">Alice</a></address>
      </header>

      <section>
        <h2>First Section</h2>
        <p>Content...</p>
      </section>

      <section>
        <h2>Second Section</h2>
        <figure>
          <img src="chart.png" alt="Sales chart showing 30% growth">
          <figcaption>Figure 1: Q4 2024 sales growth</figcaption>
        </figure>
      </section>

      <footer>
        <p>Tags: <a href="/tag/javascript">JavaScript</a></p>
      </footer>
    </article>

    <aside>
      <h2>Related Articles</h2>
      <!-- sidebar content -->
    </aside>
  </main>

  <footer>
    <p>&copy; 2024 My Site</p>
  </footer>
</body>

Element Semantics Guide

  • header: introductory content for a page section or article. Not the same as h1-h6.

  • nav: major navigation links. Not every group of links needs <nav>.

  • main: the primary content of the page. Only one per page. Skip link target.

  • article: self-contained content that would make sense standalone (blog post, product card, comment)

  • section: thematic grouping of content, typically with a heading. Use div if no heading.

  • aside: content tangentially related to surrounding content (sidebar, callout box, ads)

  • footer: footer for its parent section/article (not just the page footer)

  • figure + figcaption: self-contained content with optional caption (images, code, quotes)

  • address: contact information for nearest article or body ancestor

  • time: machine-readable date/time. Always include datetime attribute.

Interactive Elements

<!-- details/summary — native disclosure widget (accordion), no JS needed -->
<details>
  <summary>Why should I use semantic HTML?</summary>
  <p>Semantic HTML improves accessibility, SEO, and code maintainability...</p>
</details>

<!-- Open by default -->
<details open>
  <summary>Default open section</summary>
  <p>This is visible by default</p>
</details>

<!-- dialog — native modal dialog -->
<dialog id="my-modal">
  <h2>Are you sure?</h2>
  <p>This action cannot be undone.</p>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

<script>
  const dialog = document.getElementById('my-modal');
  document.getElementById('open-btn').onclick = () => dialog.showModal();
  dialog.addEventListener('close', () => {
    console.log('Result:', dialog.returnValue); // 'cancel' or 'confirm'
  });
</script>

<!-- mark — highlight text -->
<p>Search results for <mark>typescript</mark> in files.</p>

<!-- abbr — abbreviation with expansion -->
<abbr title="HyperText Markup Language">HTML</abbr>

Heading Hierarchy

  • Only one h1 per page — should match the page title

  • Don't skip heading levels (h1 → h3 without h2 is wrong)

  • Headings define document outline — use them for structure, not styling

  • Use CSS for visual sizing, not lower heading levels

  • Screen readers navigate pages by heading structure — it matters for accessibility

<!-- Correct heading hierarchy -->
<h1>JavaScript Frameworks</h1>
  <h2>React</h2>
    <h3>Hooks</h3>
    <h3>Context</h3>
  <h2>Vue</h2>
    <h3>Options API</h3>
    <h3>Composition API</h3>

<!-- WRONG — don't use headings just for size -->
<h3>This is not a subsection, I just want smaller text</h3>
<!-- Instead: -->
<p class="subtitle">This is not a subsection</p>
HTML

Forms & Input Validation

HTML Forms & Input Validation HTML5 provides extensive form capabilities including validation, many input types, and the FormData API. Understanding forms is fu

HTML Forms & Input Validation

HTML5 provides extensive form capabilities including validation, many input types, and the FormData API. Understanding forms is fundamental to web development.

Form Structure

<form action="/submit" method="post" id="signup-form" novalidate>
  <!-- novalidate disables browser's native validation UI — useful when custom validation -->

  <fieldset>
    <legend>Personal Information</legend>

    <!-- Proper label association — click label focuses input -->
    <label for="name">Full Name</label>
    <input type="text" id="name" name="name" required autocomplete="name">

    <label for="email">Email Address</label>
    <input type="email" id="email" name="email" required autocomplete="email">
  </fieldset>

  <!-- Submit button inside form is associated automatically -->
  <button type="submit">Create Account</button>

  <!-- Reset clears all fields to their default values -->
  <button type="reset">Clear Form</button>

  <!-- Button outside form can be linked via form attribute -->
</form>
<button form="signup-form" type="submit">Submit from outside</button>

Input Types

<!-- Text inputs -->
<input type="text" placeholder="Enter text">
<input type="email" autocomplete="email">
<input type="password" autocomplete="current-password" minlength="8">
<input type="tel" autocomplete="tel" pattern="[0-9]{10}">
<input type="url">
<input type="search">    <!-- shows clear button on some browsers -->

<!-- Numbers -->
<input type="number" min="0" max="100" step="5">
<input type="range" min="0" max="10" value="5">   <!-- slider -->

<!-- Dates & Times -->
<input type="date" min="2024-01-01" max="2024-12-31">
<input type="time" step="900">  <!-- step in seconds: 900 = 15min increments -->
<input type="datetime-local">
<input type="month">
<input type="week">

<!-- Pickers -->
<input type="color" value="#0070f3">
<input type="file" accept="image/*,.pdf" multiple>

<!-- Selection -->
<input type="checkbox" id="agree" name="agree" value="yes" checked>
<input type="radio" name="size" value="sm"> Small
<input type="radio" name="size" value="lg"> Large

<!-- Hidden -->
<input type="hidden" name="csrf_token" value="abc123">
<input type="hidden" name="user_id" value="42">

Validation Attributes

<!-- Built-in validation (works without JavaScript) -->
<input type="email" required>
  <!-- required: must not be empty -->
  <!-- type="email": must match email format -->

<input type="text" minlength="2" maxlength="100" required>
  <!-- minlength/maxlength: character count constraints -->

<input type="number" min="1" max="99" step="1">
  <!-- min/max: value range; step: increments -->

<input type="text" pattern="[A-Za-z]{3,}" title="At least 3 letters only">
  <!-- pattern: regex validation; title: shown in error tooltip -->

<input type="url" required>

<!-- Textarea -->
<textarea maxlength="500" required></textarea>

<!-- Custom validation with JavaScript -->
<input type="text" id="username">
<script>
  document.getElementById('username').addEventListener('input', (e) => {
    const el = e.target;
    if (!/^[a-z0-9_]+$/.test(el.value)) {
      el.setCustomValidity('Only lowercase letters, numbers, and underscores');
    } else {
      el.setCustomValidity('');  // clear error
    }
  });
</script>

Select, Datalist & Textarea

<!-- Select dropdown -->
<select name="country" required>
  <option value="" disabled selected>Choose a country</option>
  <optgroup label="Europe">
    <option value="uk">United Kingdom</option>
    <option value="de">Germany</option>
  </optgroup>
  <optgroup label="Americas">
    <option value="us">United States</option>
  </optgroup>
</select>

<!-- Multiple select -->
<select name="skills" multiple size="4">
  <option value="js">JavaScript</option>
  <option value="ts" selected>TypeScript</option>
  <option value="py">Python</option>
</select>

<!-- Datalist — autocomplete suggestions for text input -->
<input type="text" list="frameworks" name="framework">
<datalist id="frameworks">
  <option value="React">
  <option value="Vue">
  <option value="Angular">
  <option value="Svelte">
</datalist>

<!-- Textarea -->
<textarea name="message" rows="4" cols="50"
  placeholder="Your message..." maxlength="1000"></textarea>

FormData API

// Intercept form submission and send via fetch
document.getElementById('signup-form').addEventListener('submit', async (e) => {
  e.preventDefault();

  const formData = new FormData(e.target);

  // Read values
  const name = formData.get('name');        // single value
  const skills = formData.getAll('skills'); // multiple values (checkboxes)

  // Modify before sending
  formData.set('email', formData.get('email').toLowerCase());
  formData.append('timestamp', Date.now().toString());

  // Send as JSON
  const data = Object.fromEntries(formData.entries());
  const res = await fetch('/api/signup', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  // Send as multipart/form-data (for file uploads)
  const res2 = await fetch('/api/upload', {
    method: 'POST',
    body: formData,  // don't set Content-Type — browser sets boundary automatically
  });
});
HTML

Tables & Lists

HTML Tables & Lists Accessible Table Structure <table> <caption>Q4 2024 Sales by Region</caption> <!-- table title for screen readers --> <thead> <tr> <th scope

HTML Tables & Lists

Accessible Table Structure

<table>
  <caption>Q4 2024 Sales by Region</caption>  <!-- table title for screen readers -->

  <thead>
    <tr>
      <th scope="col">Region</th>
      <th scope="col">Q3</th>
      <th scope="col" aria-sort="descending">Q4</th>  <!-- sorted column -->
      <th scope="col">Change</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <th scope="row">Europe</th>   <!-- row header -->
      <td>$1.2M</td>
      <td>$1.8M</td>
      <td><span aria-label="up 50%">↑ 50%</span></td>
    </tr>
    <tr>
      <th scope="row">Americas</th>
      <td>$2.1M</td>
      <td>$2.4M</td>
      <td><span aria-label="up 14%">↑ 14%</span></td>
    </tr>
  </tbody>

  <tfoot>
    <tr>
      <th scope="row">Total</th>
      <td>$3.3M</td>
      <td>$4.2M</td>
      <td><span aria-label="up 27%">↑ 27%</span></td>
    </tr>
  </tfoot>
</table>

Spanning Cells

<table>
  <tr>
    <th colspan="3">2024 Summary</th>  <!-- spans 3 columns -->
  </tr>
  <tr>
    <th>Q1</th>
    <th>Q2</th>
    <th>Q3</th>
  </tr>
  <tr>
    <td rowspan="2">$1M</td>  <!-- spans 2 rows -->
    <td>$1.2M</td>
    <td>$1.4M</td>
  </tr>
  <tr>
    <!-- first cell is occupied by rowspan above -->
    <td>$1.3M</td>
    <td>$1.5M</td>
  </tr>
</table>

Complex Tables with headers attribute

<!-- For complex tables where scope is ambiguous -->
<table>
  <tr>
    <td></td>
    <th id="mon">Monday</th>
    <th id="tue">Tuesday</th>
  </tr>
  <tr>
    <th id="morning">Morning</th>
    <td headers="mon morning">9am meeting</td>
    <td headers="tue morning">Code review</td>
  </tr>
  <tr>
    <th id="afternoon">Afternoon</th>
    <td headers="mon afternoon">Sprint planning</td>
    <td headers="tue afternoon">1:1</td>
  </tr>
</table>

Lists

<!-- Unordered list — bullet points -->
<ul>
  <li>Item one</li>
  <li>Item two
    <ul>
      <li>Nested item</li>
    </ul>
  </li>
</ul>

<!-- Ordered list — numbered -->
<ol>
  <li>First step</li>
  <li>Second step</li>
</ol>

<!-- Ordered list attributes -->
<ol type="A">    <!-- A, B, C ... -->
<ol type="i">    <!-- i, ii, iii ... -->
<ol start="5">   <!-- start from 5 -->
<ol reversed>    <!-- count down -->

<!-- Definition list — term and definition pairs -->
<dl>
  <dt>HTML</dt>
  <dd>HyperText Markup Language — the structure of web pages</dd>

  <dt>CSS</dt>
  <dd>Cascading Style Sheets — the presentation layer</dd>

  <dt>HTTP</dt>
  <dt>HTTPS</dt>  <!-- multiple terms can share a definition -->
  <dd>Protocol for web communication</dd>
</dl>

Table Accessibility Checklist

  • Always include <caption> or aria-label describing the table's purpose

  • Use <th> with scope="col" for column headers and scope="row" for row headers

  • Use <thead>, <tbody>, <tfoot> to structure the table semantically

  • Avoid using tables for layout — use CSS Grid or Flexbox instead

  • Don't use empty <th> cells — use aria-hidden="true" if truly needed

  • Test with screen reader (NVDA/VoiceOver) — tables are announced differently by different readers

  • Avoid complex merged cells when simpler layout would work

HTML

Media & Embedded Content

HTML: Media & Embedded Content Images — Responsive & Performant <!-- Basic image with required alt --> <img src="photo.jpg" alt="Alice presenting at a conferenc

HTML: Media & Embedded Content

Images — Responsive & Performant

<!-- Basic image with required alt -->
<img src="photo.jpg" alt="Alice presenting at a conference">

<!-- Decorative image — empty alt, not described to screen readers -->
<img src="decorative-line.svg" alt="">

<!-- Responsive images — let browser choose optimal size -->
<img
  src="hero-800.jpg"
  srcset="
    hero-400.jpg  400w,
    hero-800.jpg  800w,
    hero-1200.jpg 1200w,
    hero-1600.jpg 1600w
  "
  sizes="
    (max-width: 600px) 100vw,
    (max-width: 1200px) 50vw,
    800px
  "
  alt="Hero image"
  width="1200"
  height="600"
  loading="lazy"
  decoding="async">

<!-- Always include width/height to prevent layout shift (CLS) -->
<!-- loading="lazy" defers offscreen images until near viewport -->
<!-- decoding="async" doesn't block main thread during image decode -->

picture Element — Art Direction

<!-- Different image crops for different screen sizes -->
<picture>
  <!-- Mobile: tall portrait crop -->
  <source
    media="(max-width: 600px)"
    srcset="hero-mobile.webp"
    type="image/webp">

  <!-- Tablet: square crop -->
  <source
    media="(max-width: 1024px)"
    srcset="hero-tablet.webp"
    type="image/webp">

  <!-- Desktop: wide landscape -->
  <source srcset="hero-desktop.webp" type="image/webp">

  <!-- Fallback for browsers not supporting WebP -->
  <img src="hero-desktop.jpg" alt="Product hero image" width="1440" height="600">
</picture>

<!-- Modern format with JPEG fallback -->
<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="A beautiful landscape">
</picture>

Video

<video
  controls
  width="800"
  height="450"
  poster="thumbnail.jpg"
  preload="metadata"    <!-- none | metadata | auto -->
  playsinline           <!-- plays inline on iOS, not fullscreen -->
  loop
  muted                 <!-- required for autoplay to work in most browsers -->
  autoplay>

  <source src="video.webm" type="video/webm">
  <source src="video.mp4" type="video/mp4">

  <!-- Subtitles / closed captions (required for accessibility) -->
  <track kind="subtitles" src="captions.vtt" srclang="en" label="English" default>
  <track kind="subtitles" src="captions-uk.vtt" srclang="uk" label="Ukrainian">

  <p>Your browser doesn't support HTML video.
    <a href="video.mp4">Download the video</a></p>
</video>

Audio

<audio controls preload="none">
  <source src="podcast.opus" type="audio/ogg; codecs=opus">
  <source src="podcast.mp3" type="audio/mpeg">
  <p>Your browser doesn't support HTML audio.</p>
</audio>

<!-- Autoplay audio is blocked by browsers unless muted -->
<audio autoplay muted loop>
  <source src="ambient.mp3" type="audio/mpeg">
</audio>

iframe — Embedding External Content

<!-- Embed a YouTube video (privacy-enhanced mode) -->
<iframe
  width="560"
  height="315"
  src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
  title="Video title"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
  allowfullscreen
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin">
</iframe>

<!-- Sandbox — restricts iframe capabilities -->
<iframe
  src="https://example.com/widget"
  sandbox="allow-scripts allow-same-origin allow-forms"
  title="Widget description">
  <!-- sandbox values (additive):
    allow-scripts        — run JavaScript
    allow-same-origin    — maintain origin (for cookie access, localStorage)
    allow-forms          — submit forms
    allow-popups         — window.open()
    allow-top-navigation — navigate parent frame
  -->
</iframe>

canvas Basics

<canvas id="myCanvas" width="800" height="400">
  <!-- Fallback content for non-canvas browsers -->
  Your browser doesn't support Canvas.
</canvas>

<script>
  const canvas = document.getElementById('myCanvas');
  const ctx = canvas.getContext('2d');

  // Shapes
  ctx.fillStyle = '#0070f3';
  ctx.fillRect(10, 10, 100, 50);  // x, y, width, height

  ctx.strokeStyle = '#ff0000';
  ctx.lineWidth = 3;
  ctx.strokeRect(120, 10, 100, 50);

  // Path
  ctx.beginPath();
  ctx.moveTo(10, 100);
  ctx.lineTo(100, 150);
  ctx.lineTo(50, 200);
  ctx.closePath();
  ctx.fill();

  // Text
  ctx.font = '24px sans-serif';
  ctx.fillStyle = '#000';
  ctx.fillText('Hello Canvas', 10, 300);

  // Image
  const img = new Image();
  img.onload = () => ctx.drawImage(img, 200, 10, 200, 100);
  img.src = 'photo.jpg';
</script>
HTML

HTML APIs & Web Platform

HTML APIs & Web Platform Web Storage // localStorage — persists until explicitly cleared (survives browser restarts) localStorage.setItem('user_prefs', JSON.str

HTML APIs & Web Platform

Web Storage

// localStorage — persists until explicitly cleared (survives browser restarts)
localStorage.setItem('user_prefs', JSON.stringify({ theme: 'dark', lang: 'en' }));
const prefs = JSON.parse(localStorage.getItem('user_prefs') ?? '{}');
localStorage.removeItem('user_prefs');
localStorage.clear();  // clears ALL keys for this origin

// sessionStorage — cleared when tab is closed
sessionStorage.setItem('draft', JSON.stringify(formData));
const draft = JSON.parse(sessionStorage.getItem('draft') ?? 'null');
sessionStorage.removeItem('draft');

// Both have the same API. Key differences:
// localStorage: 5-10MB limit, shared across all tabs for the same origin
// sessionStorage: 5-10MB limit, isolated per tab

// Listen for storage events (fires in OTHER tabs, not the one that changed it)
window.addEventListener('storage', (event) => {
  console.log('Key changed:', event.key);
  console.log('Old value:', event.oldValue);
  console.log('New value:', event.newValue);
});

History API

// Navigate without page reload
history.pushState({ page: 'about' }, '', '/about');    // adds to history
history.replaceState({ page: 'home' }, '', '/');        // replaces current entry

// Go back/forward programmatically
history.back();
history.forward();
history.go(-2);  // go back 2 steps

// Handle browser back/forward button
window.addEventListener('popstate', (event) => {
  console.log('Navigated to state:', event.state);
  renderPage(event.state);
});

// Current URL info
const url = new URL(window.location.href);
console.log(url.pathname);    // /about
console.log(url.searchParams.get('q'));  // query param
url.searchParams.set('page', '2');
history.pushState({}, '', url.toString());

Intersection Observer

// Detect when elements enter/leave the viewport
// Use for: lazy loading, infinite scroll, animation triggers, analytics

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
      entry.target.src = entry.target.dataset.src;  // lazy load image
      observer.unobserve(entry.target);  // stop observing after first trigger
    }
  });
}, {
  root: null,          // viewport
  rootMargin: '200px', // start loading 200px before entering viewport
  threshold: 0.1,      // fire when 10% of element is visible
});

// Observe all lazy images
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));

// Infinite scroll
const sentinel = document.querySelector('#load-more');
const infiniteObserver = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) loadMoreItems();
}, { threshold: 1.0 });
infiniteObserver.observe(sentinel);

MutationObserver

// Watch for DOM changes (added/removed nodes, attribute changes)
const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach(node => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          console.log('Element added:', node.tagName);
        }
      });
    }
    if (mutation.type === 'attributes') {
      console.log('Attribute changed:', mutation.attributeName);
    }
  });
});

observer.observe(document.body, {
  childList: true,    // child node additions/removals
  subtree: true,      // observe all descendants
  attributes: true,   // attribute changes
  attributeFilter: ['class', 'data-theme'],  // only these attributes
  characterData: true, // text content changes
});

// Disconnect when done
observer.disconnect();

Clipboard API

// Write to clipboard
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('Copied!');
  } catch (err) {
    // Fallback for older browsers
    const el = document.createElement('textarea');
    el.value = text;
    document.body.appendChild(el);
    el.select();
    document.execCommand('copy');
    document.body.removeChild(el);
  }
}

// Read from clipboard
async function readFromClipboard() {
  const text = await navigator.clipboard.readText();
  return text;
}

// Copy image to clipboard
async function copyImage(blob) {
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': blob })
  ]);
}

Drag & Drop API

// Make element draggable
const draggable = document.querySelector('.draggable');
draggable.setAttribute('draggable', 'true');

draggable.addEventListener('dragstart', (e) => {
  e.dataTransfer.setData('text/plain', e.target.id);
  e.dataTransfer.effectAllowed = 'move';
});

// Drop target
const dropZone = document.querySelector('.drop-zone');

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault();  // allow drop
  e.dataTransfer.dropEffect = 'move';
  dropZone.classList.add('drag-over');
});

dropZone.addEventListener('dragleave', () => {
  dropZone.classList.remove('drag-over');
});

dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  const id = e.dataTransfer.getData('text/plain');
  const dragged = document.getElementById(id);
  dropZone.appendChild(dragged);
  dropZone.classList.remove('drag-over');
});
HTML

Accessibility (ARIA & a11y)

HTML Accessibility: ARIA & a11y Accessibility (a11y) ensures your web content can be used by everyone, including people with disabilities. The first rule of ARI

HTML Accessibility: ARIA & a11y

Accessibility (a11y) ensures your web content can be used by everyone, including people with disabilities. The first rule of ARIA: don't use ARIA if native HTML can do the job.

Why Accessibility Matters

  • ~15% of the world's population has some form of disability — that's over 1 billion people

  • Legal requirements: ADA (US), EAA (EU), AODA (Canada), WCAG 2.1 AA is the typical standard

  • Better UX for everyone: keyboard navigation, captions, sufficient contrast help all users

  • SEO benefit: semantic HTML and ARIA help search engines understand content

  • Test with: VoiceOver (macOS/iOS), NVDA (Windows), axe DevTools, Lighthouse, keyboard-only browsing

ARIA Basics

<!-- ARIA roles, states, and properties supplement or override native semantics -->

<!-- role — what the element is -->
<div role="button" tabindex="0">Click me</div>
<!-- Better: just use <button> — has role, keyboard support, and semantics built-in -->

<!-- aria-label — name for elements without visible text -->
<button aria-label="Close dialog">✕</button>
<input type="search" aria-label="Search products">
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Footer navigation">...</nav>

<!-- aria-labelledby — associate with another element's text -->
<h2 id="section-title">User Settings</h2>
<section aria-labelledby="section-title">...</section>

<!-- aria-describedby — additional description (announced after label) -->
<input type="password" id="pwd" aria-describedby="pwd-hint">
<p id="pwd-hint">Must be at least 8 characters with one number.</p>

<!-- aria-hidden — hide from accessibility tree (decorative elements) -->
<span aria-hidden="true">👋</span>
<i class="icon-star" aria-hidden="true"></i>

<!-- aria-live — announce dynamic content changes -->
<div aria-live="polite">   <!-- announce after current speech -->
  Loading results...
</div>
<div role="alert" aria-live="assertive">   <!-- interrupt immediately -->
  Error: Form submission failed.
</div>

Interactive ARIA Patterns

<!-- Custom dropdown/select -->
<button aria-haspopup="listbox" aria-expanded="false" aria-controls="dropdown-list" id="dropdown-btn">
  Select option
</button>
<ul role="listbox" id="dropdown-list" aria-labelledby="dropdown-btn" hidden>
  <li role="option" aria-selected="false">Option 1</li>
  <li role="option" aria-selected="true">Option 2</li>
</ul>

<!-- Tabs -->
<div role="tablist" aria-label="Settings tabs">
  <button role="tab" aria-selected="true" aria-controls="tab-general" id="tab-general-btn">General</button>
  <button role="tab" aria-selected="false" aria-controls="tab-security" id="tab-security-btn" tabindex="-1">Security</button>
</div>
<div role="tabpanel" id="tab-general" aria-labelledby="tab-general-btn">...</div>
<div role="tabpanel" id="tab-security" aria-labelledby="tab-security-btn" hidden>...</div>

<!-- Dialog/Modal -->
<div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc">
  <h2 id="modal-title">Confirm Deletion</h2>
  <p id="modal-desc">This action cannot be undone. Are you sure?</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

<!-- Toggle button -->
<button aria-pressed="false">Mute</button>
<button aria-expanded="false" aria-controls="sidebar">Menu</button>

Keyboard Navigation

// Trap focus inside modal while it's open
function trapFocus(element) {
  const focusable = element.querySelectorAll(
    'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  element.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;

    if (e.shiftKey) {
      if (document.activeElement === first) {
        last.focus();
        e.preventDefault();
      }
    } else {
      if (document.activeElement === last) {
        first.focus();
        e.preventDefault();
      }
    }
  });
}

// Close modal with Escape
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') closeModal();
});

// Arrow key navigation for tab/menu components
menuContainer.addEventListener('keydown', (e) => {
  const items = [...menuContainer.querySelectorAll('[role="menuitem"]')];
  const current = items.indexOf(document.activeElement);

  if (e.key === 'ArrowDown') items[(current + 1) % items.length].focus();
  if (e.key === 'ArrowUp') items[(current - 1 + items.length) % items.length].focus();
  if (e.key === 'Home') items[0].focus();
  if (e.key === 'End') items[items.length - 1].focus();
});

Skip Links & Focus Management

<!-- Skip link — allows keyboard users to skip repeated navigation -->
<a href="#main-content" class="skip-link">Skip to main content</a>

<nav>...</nav>

<main id="main-content" tabindex="-1">
  <!-- tabindex="-1" allows programmatic focus without adding to tab order -->
  Content...
</main>

<style>
.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px 16px;
  z-index: 9999;
}
.skip-link:focus {
  top: 0;  /* visible only when focused */
}
</style>

Color Contrast & Visual

  • WCAG AA: 4.5:1 contrast ratio for normal text (< 18px), 3:1 for large text (≥ 18px or bold ≥ 14px)

  • WCAG AAA: 7:1 for normal text, 4.5:1 for large text

  • Check with: WebAIM Contrast Checker, Chrome DevTools → CSS Overview → Colors

  • Don't rely solely on color to convey meaning (e.g., error state) — add icon or text

  • Focus indicators must be visible — never remove :focus outline without providing custom alternative

  • Minimum tap target size: 44×44px (iOS HIG), 48×48dp (Material Design)

HTML

SEO, Performance & Best Practices

HTML: SEO, Performance & Best Practices Structured Data (JSON-LD) JSON-LD is the preferred format for structured data. Google uses it to create rich results in

HTML: SEO, Performance & Best Practices

Structured Data (JSON-LD)

JSON-LD is the preferred format for structured data. Google uses it to create rich results in search (review stars, FAQ dropdowns, recipe cards, etc.).

<!-- Article schema -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "How to Use HTML Semantic Elements",
  "datePublished": "2024-01-15",
  "dateModified": "2024-02-01",
  "author": {
    "@type": "Person",
    "name": "Alice Johnson",
    "url": "https://example.com/authors/alice"
  },
  "publisher": {
    "@type": "Organization",
    "name": "My Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://example.com/logo.png"
    }
  },
  "image": "https://example.com/article-image.jpg",
  "description": "A comprehensive guide to semantic HTML elements"
}
</script>

<!-- Breadcrumb schema -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {"@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com"},
    {"@type": "ListItem", "position": 2, "name": "Blog", "item": "https://example.com/blog"},
    {"@type": "ListItem", "position": 3, "name": "This Article"}
  ]
}
</script>

Performance — Core Web Vitals

  • LCP (Largest Contentful Paint): how fast the largest visible element loads. Target < 2.5s. Fix: preload hero image, fast server response (TTFB), no render-blocking resources.

  • INP (Interaction to Next Paint): responsiveness to clicks/key presses. Target < 200ms. Fix: reduce main thread blocking, defer heavy JavaScript.

  • CLS (Cumulative Layout Shift): visual stability. Target < 0.1. Fix: always include width/height on images, don't inject content above existing content, use CSS aspect-ratio.

Preventing Layout Shift (CLS)

<!-- Always specify dimensions — browser reserves space before image loads -->
<img src="photo.jpg" alt="..." width="800" height="600" loading="lazy">

<!-- CSS aspect-ratio for responsive images -->
<style>
img {
  width: 100%;
  height: auto;
  aspect-ratio: 4/3;  /* maintains ratio even before image loads */
}
</style>

<!-- Font loading — prevent text flash/shift -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<style>
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;  /* show fallback immediately, swap when loaded */
  /* optional: avoid swap shift if font loads late */
  font-display: optional;
}
</style>

Resource Hints & Loading

<!-- Preload: high-priority fetch of critical resources -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/inter.woff2" as="font" crossorigin>

<!-- Preconnect: early connection to third-party origins (saves 100-500ms) -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com">

<!-- DNS-prefetch: cheaper than preconnect, just resolves DNS -->
<link rel="dns-prefetch" href="https://cdn.example.com">

<!-- Prefetch: low-priority fetch for next navigation -->
<link rel="prefetch" href="/next-page.html">

<!-- fetchpriority attribute (Chrome 101+) -->
<img src="hero.jpg" fetchpriority="high" alt="Hero">  <!-- bump priority -->
<img src="below-fold.jpg" fetchpriority="low" alt="Below fold">
<link rel="preload" href="non-critical.js" as="script" fetchpriority="low">

Script & Style Optimization

<!-- Scripts: always defer unless critical -->
<script src="app.js" defer></script>          <!-- recommended -->
<script src="analytics.js" async></script>    <!-- order-independent scripts -->

<!-- Critical CSS inline + load rest async -->
<style>/* inline above-the-fold CSS here */</style>
<link rel="stylesheet" href="full.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="full.css"></noscript>

<!-- Module preload -->
<link rel="modulepreload" href="/app.mjs">

<!-- Lazy load iframes -->
<iframe src="..." loading="lazy"></iframe>

SEO Best Practices Checklist

  • Unique <title> per page: primary keyword + brand name, 50-60 characters

  • Meta description: 120-155 characters, compelling, includes call to action

  • Canonical URL on every page — prevents duplicate content penalty

  • One h1 per page, matching the page topic

  • All images have descriptive alt text (empty alt for decorative images)

  • hreflang tags for multi-language sites

  • Sitemap.xml submitted to Google Search Console

  • robots.txt controlling what search engines can crawl

  • Structured data (JSON-LD) for articles, products, FAQs, breadcrumbs

  • HTTPS (Google uses HTTPS as a ranking signal)

  • Mobile-friendly (viewport meta tag, responsive design) — Google uses mobile-first indexing

  • Fast LCP: Core Web Vitals are ranking signals

Keep your HTML knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever