Semantic HTML — Cheat Sheet
HTML, CSS & Responsive · 5 topics. Download the PDF or the Instagram carousel and share it.
Document Structure and Landmarks
The element you choose is an API. A div says nothing; a nav, a main and a heading tell the browser, the screen reader and Google what the page is.
- ✓lang on the html element decides which voice a screen reader uses; without it English is read with the wrong phonetics
- ✓Without the viewport meta tag a phone renders at 980px and every responsive rule is ignored
- ✓header, nav, main, aside and footer are landmarks a screen-reader user can jump between — exactly one main per page
- ✓Headings are an outline: one h1, no skipped levels, ordered by meaning rather than by size
- ✓A section without a heading should be a div; the element you choose is information, not decoration
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Two Sum — AiCanCode</title> <meta name="description" content="Solve Two Sum with a hash map in O(n)."> <link rel="canonical" href="https://aicancode.org/dsa/two-sum"> </head> <body> … </body> </html> // lang="en" tells a screen reader which voice to use. Without it, // English is read with the user's default language phonetics — // which is unintelligible. // Without the viewport meta, a phone renders the page at 980px and // zooms out. Every responsive rule you write is then ignored. // title is the browser tab, the bookmark, the search result heading // and the first thing a screen reader announces. Front-load the // specific part: "Two Sum — AiCanCode", not "AiCanCode — Two Sum".
Text, Links and Buttons
A link goes somewhere, a button does something. Getting that one distinction right fixes more accessibility problems than any ARIA attribute.
- ✓A link changes the URL; a button acts on the current page — that single question decides the element
- ✓A button inside a form defaults to type="submit", which is why a Cancel button can reload the page
- ✓Link text is the accessible name and is heard out of context, so "click here" is unusable
- ✓target="_blank" needs rel="noopener" and a visible or screen-reader warning
- ✓strong and em carry announced meaning; b and i are purely visual, and lists announce their item count
// Navigates -> a link <a href="/dsa/problems">All problems</a> // Performs an action on this page -> a button <button type="button" onclick="openDialog()">Show hint</button> // The differences you get for free with the right element: // <a href> <button> // keyboard Enter Enter AND Space // Ctrl+click opens a new tab nothing // history yes no // announced "link" "button" // right-click copy link address nothing // The two failures: <a href="#" onclick="save()">Save</a> // jumps to top, adds history <div onclick="go()">Go</div> // unreachable by keyboard // Inside a form, button defaults to type="submit" — // which is why a "Cancel" button reloads the page: <button>Cancel</button> // submits! <button type="button">Cancel</button> // correct
Forms — Labels, Input Types and Autofill
The right input type gives a phone the right keyboard, the right autocomplete lets a browser fill a form in one tap, and a real label makes the field usable at all.
- ✓Every input needs a real label — a placeholder is not announced, vanishes on typing and fails contrast
- ✓A label also enlarges the tap target, which matters most on phones
- ✓The input type sets the mobile keyboard and native validation; type="number" is wrong for phone numbers, OTPs and PINs
- ✓inputmode picks the keyboard without changing validation semantics
- ✓autocomplete tokens let the browser and password manager fill a form in one tap — current-password and new-password are the important pair
<label for="email">Email address</label> <input id="email" name="email" type="email"> // Or wrap, which needs no id <label> Email address <input name="email" type="email"> </label> // A real label also expands the click target — tapping the text // focuses the field, which matters a lot on a phone. // Placeholder as label: fails on four counts <input placeholder="Email address"> // no announced name, // disappears on typing, low contrast, and looks pre-filled. // Visually hidden when the design has no room — still announced <label for="q" class="sr-only">Search problems</label> <input id="q" type="search" placeholder="Search…"> // Help text and errors, linked so they are announced with the field <input id="pw" aria-describedby="pw-help pw-error" aria-invalid="true"> <p id="pw-help">At least 8 characters.</p> <p id="pw-error" role="alert">Include a number.</p> // Group related controls <fieldset><legend>Difficulty</legend> …radios… </fieldset>
Tables — Real Data, and Making It Fit a Phone
A table is the right element for tabular data and the wrong one for layout. Headers with scope make it navigable; a phone needs a deliberate strategy, not a horizontal scrollbar by accident.
- ✓scope="col" and scope="row" let a screen reader announce the header with each cell
- ✓caption is the table's accessible name and is announced when a user enters it
- ✓A horizontally scrolling table wrapper needs tabindex and a label, or keyboard users cannot scroll it
- ✓Stacking rows into cards with data-label pseudo-content is the usual small-screen alternative
- ✓Use a table when rows and columns intersect meaningfully; a div grid means writing every ARIA role by hand
<table>
<caption>Submissions this week</caption>
<thead>
<tr>
<th scope="col">Problem</th>
<th scope="col">Difficulty</th>
<th scope="col">Runtime</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Two Sum</th> <!-- the row's own header -->
<td>Easy</td>
<td>42 ms</td>
</tr>
</tbody>
<tfoot><tr><td colspan="3">3 of 12 solved</td></tr></tfoot>
</table>
// scope="col" and scope="row" are what let a screen reader say
// "Two Sum, Difficulty, Easy" instead of just "Easy".
// caption is the table's accessible name, announced on entry.
// It also survives when the surrounding heading does not.
// Numbers align right and use tabular figures so columns line up:
td.num { text-align: right; font-variant-numeric: tabular-nums }Images, Media and Page Metadata
Images are usually the heaviest thing on a page and the most common cause of layout shift. Metadata decides how the page looks in a search result and in a shared link.
- ✓width and height on images reserve space and prevent the layout shift that dominates CLS
- ✓alt describes purpose — empty for decorative images, and never omitted, or the filename is read aloud
- ✓loading="lazy" belongs below the fold; on the hero image it delays LCP, so preload that one instead
- ✓Captions serve far more people than deaf users, and autoplay with sound is blocked and unwelcome
- ✓Open Graph tags control link previews and og:image must be an absolute URL or the preview is blank
<img src="cover.webp" width="640" height="360" alt="" loading="lazy" decoding="async"> // width and height prevent layout shift — the browser reserves the // space from the ratio before the file arrives. This is the single // biggest cause of CLS. // alt describes PURPOSE, not appearance <img alt=""> // decorative — correctly empty <img alt="Akshay Sonalkar"> // a portrait <img alt="Solved problems rose from 12 to 48"> // a chart: state the finding <img alt="Two Sum solution screenshot"> // not "image of a screenshot" // Never omit alt entirely — a screen reader then reads the filename. // Modern formats and responsive sources <picture> <source type="image/avif" srcset="hero.avif"> <source type="image/webp" srcset="hero.webp"> <img src="hero.jpg" alt="…" width="1200" height="630"> </picture> // loading="lazy" for below the fold; NEVER on the hero image — // it delays your LCP. Preload that one instead: <link rel="preload" as="image" href="hero.webp" fetchpriority="high">