html · chain · relay · step 060 of 64
offline render, Chrome 154, light scheme, 2 s of virtual time
This page calls Math.random or the clock, so a live run can differ from this render.
runs in a sandboxed frame with no origin; live pages may fetch CDN fonts the offline render did not
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="content-security-policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="description" content="A well-structured, accessible HTML5 document demonstrating modern best practices for semantic markup and inclusive design.">
<meta name="author" content="K. Smith">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">
<meta property="og:title" content="Welcome - Accessible HTML5 Guide">
<meta property="og:description" content="Modern HTML5 demonstrating accessibility, semantic markup, and responsive design.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://www.example.com">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Welcome - Accessible HTML5 Guide">
<meta name="twitter:description" content="Modern HTML5 demonstrating accessibility, semantic markup, and responsive design.">
<title>Welcome - Accessible HTML5 Guide</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "Accessible HTML5 Guide",
"description": "A well-structured, accessible HTML5 document demonstrating modern best practices for semantic markup and inclusive design.",
"author": {
"@type": "Person",
"name": "K. Smith"
}
}
</script>
<script>
try {
const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'light' || storedTheme === 'dark') {
document.documentElement.dataset.theme = storedTheme;
}
} catch {}
</script>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E👋%3C/text%3E%3C/svg%3E">
<link rel="canonical" href="https://www.example.com">
<style>
:root {
color-scheme: light dark;
--text: #1a1a1a;
--text-secondary: #555;
--background: #fff;
--surface: #f9f9f9;
--accent: #0052a3;
--border: #e0e0e0;
--border-control: #767676;
--error: #c62828;
--success: #1b5e20;
}
@media screen and (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--text: #e0e0e0;
--text-secondary: #a0a0a0;
--background: #121212;
--surface: #1e1e1e;
--accent: #5fa3ff;
--border: #333;
--border-control: #8c8c8c;
--error: #ff8a80;
--success: #66bb6a;
}
}
@media screen {
:root[data-theme="light"] {
color-scheme: light;
}
:root[data-theme="dark"] {
color-scheme: dark;
--text: #e0e0e0;
--text-secondary: #a0a0a0;
--background: #121212;
--surface: #1e1e1e;
--accent: #5fa3ff;
--border: #333;
--border-control: #8c8c8c;
--error: #ff8a80;
--success: #66bb6a;
}
}
[hidden] {
display: none !important;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
a {
transition: text-decoration-thickness 0.15s ease;
}
.skip-link {
transition: transform 0.15s ease;
}
button,
.button {
transition: filter 0.15s ease, transform 0.15s ease;
}
input,
textarea {
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
}
body {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
min-height: 100vh;
min-height: 100dvh;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 1rem;
line-height: 1.6;
overflow-wrap: break-word;
max-width: 70ch;
margin: 0 auto;
padding-top: max(clamp(1rem, 5vw, 1.5rem), env(safe-area-inset-top));
padding-right: max(clamp(1rem, 5vw, 1.5rem), env(safe-area-inset-right));
padding-bottom: max(clamp(1rem, 5vw, 1.5rem), env(safe-area-inset-bottom));
padding-left: max(clamp(1rem, 5vw, 1.5rem), env(safe-area-inset-left));
color: var(--text);
background: var(--background);
}
h1 {
color: var(--accent);
font-size: clamp(1.75rem, 1.25rem + 3vw, 2.5rem);
font-weight: 700;
line-height: 1.1;
text-wrap: balance;
margin: 0 0 1.5rem 0;
letter-spacing: -0.02em;
}
h2 {
color: var(--accent);
font-size: clamp(1.25rem, 1rem + 1.5vw, 1.75rem);
font-weight: 600;
line-height: 1.2;
text-wrap: balance;
margin: 2rem 0 1rem 0;
letter-spacing: -0.01em;
}
h2:first-child {
margin-top: 0;
}
p {
text-wrap: pretty;
margin: 1rem 0;
line-height: 1.65;
}
a {
color: var(--accent);
text-underline-offset: 0.2em;
text-decoration-thickness: 2px;
text-decoration-skip-ink: auto;
font-weight: 500;
}
a:active {
text-decoration-thickness: 3px;
}
@media (hover: hover) {
a:hover {
text-decoration-thickness: 3px;
}
}
a[target="_blank"]::after {
content: "\2197";
content: "\2197" / "";
display: inline-block;
margin-inline-start: 0.2em;
font-size: 0.8em;
text-decoration: none;
}
button,
.button {
display: inline-block;
text-decoration: none;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 500;
font-family: inherit;
color: var(--background);
background: var(--accent);
border: 2px solid var(--accent);
border-radius: 0.25rem;
cursor: pointer;
white-space: nowrap;
user-select: none;
}
@media (hover: hover) {
button:not(:disabled, [aria-disabled="true"]):hover,
.button:hover {
filter: brightness(1.15);
}
}
button:not(:disabled, [aria-disabled="true"]):active,
.button:active {
transform: scale(0.97);
}
button:disabled,
button[aria-disabled="true"] {
opacity: 0.6;
cursor: not-allowed;
}
ul, ol {
margin: 1rem 0;
padding-inline-start: 2rem;
}
li {
margin: 0.5rem 0;
}
:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
a:not(.button):focus-visible {
border-radius: 2px;
}
.skip-link {
position: absolute;
inset-inline-start: 0;
top: 0;
transform: translateY(-100%);
background: var(--accent);
color: var(--background);
padding: 0.75rem 1.5rem;
z-index: 1000;
border-end-end-radius: 0.25rem;
font-weight: 600;
text-decoration: none;
display: inline-block;
}
.skip-link:focus {
transform: translateY(0);
outline: 3px solid var(--text);
outline-offset: -3px;
box-shadow: inset 0 0 0 2px var(--background);
}
header {
padding-block: 1.5rem 0;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.header-bar h1 {
margin: 0;
}
.theme-toggle {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 2.75rem;
height: 2.75rem;
padding: 0;
font-size: 1.25rem;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border-control);
}
nav ul {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.5rem;
margin: 1rem 0 0 0;
padding: 0;
list-style: none;
}
nav li {
margin: 0;
}
footer nav ul {
justify-content: center;
margin: 0 0 1rem 0;
}
main {
padding: 1rem 0;
}
main:focus {
outline: none;
}
section {
margin: 2rem 0;
}
[id] {
scroll-margin-top: 1.5rem;
}
@media (forced-colors: active) {
.skip-link,
button,
.button {
border: 2px solid ButtonText;
}
/* System colors override the page theme, so the toggle would do nothing. */
.theme-toggle {
display: none;
}
}
.card {
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
background: var(--surface);
margin: 1.5rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.card h3 {
font-size: 1.25rem;
line-height: 1.3;
text-wrap: balance;
margin-top: 0;
margin-bottom: 0.5rem;
color: var(--accent);
}
table {
width: 100%;
margin: 1rem 0;
border-collapse: collapse;
}
caption {
margin-bottom: 0.5rem;
font-weight: 500;
text-align: start;
}
th,
td {
padding: 0.75rem;
text-align: center;
border-bottom: 1px solid var(--border);
}
th:first-child {
text-align: start;
}
thead th {
border-bottom-width: 2px;
font-weight: 700;
background: var(--surface);
}
tbody tr:last-child > * {
border-bottom: none;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
@media (prefers-contrast: more) {
/* Doubled to outrank the dark theme selectors, which would otherwise reset these borders. */
:root:root {
--border: currentColor;
--border-control: currentColor;
}
button,
.button {
border-width: 3px;
}
.card {
border-width: 2px;
}
}
footer {
padding-block: 1.5rem;
border-top: 1px solid var(--border);
font-size: 0.875rem;
color: var(--text-secondary);
text-align: center;
}
footer p {
margin: 0.5rem 0;
}
small {
display: block;
}
kbd {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.25rem;
padding: 0.125rem 0.375rem;
font-family: monospace;
font-size: 0.875em;
}
fieldset {
border: none;
padding: 0;
margin: 0 0 1rem 0;
}
legend {
display: block;
padding: 0;
font-weight: 600;
margin-bottom: 1rem;
}
.field {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
input,
textarea {
width: 100%;
padding: 0.5rem;
font: inherit;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border-control);
border-radius: 0.25rem;
}
textarea {
resize: vertical;
}
#form-status {
margin: 0;
}
#form-status:not(:empty) {
margin-top: 1rem;
padding: 0.75rem 1rem;
border-radius: 0.25rem;
background: var(--surface);
background: color-mix(in srgb, var(--success) 10%, transparent);
color: var(--success);
border: 2px solid var(--success);
animation: slideDown 0.3s ease;
font-weight: 500;
}
#form-status[data-state="pending"] {
background: var(--background);
color: var(--text-secondary);
border-color: var(--border-control);
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
#form-status:not(:empty) {
animation: none;
}
}
#form-status[data-state="error"] {
background: color-mix(in srgb, var(--error) 10%, transparent);
color: var(--error);
border-color: var(--error);
border-width: 2px;
font-weight: 500;
}
.form-hint {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: var(--text-secondary);
}
.field .form-hint {
margin: 0.25rem 0 0 0;
}
.error-message {
display: block;
color: var(--error);
font-size: 0.875rem;
font-weight: 500;
}
.error-message:not(:empty) {
margin-top: 0.25rem;
}
.counter {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
margin: 1.5rem 0;
}
.counter output {
min-width: 4ch;
font-size: 1.75rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--accent);
text-align: center;
padding: 0.5rem;
background: var(--surface);
border-radius: 0.25rem;
border: 2px solid var(--border);
}
.counter #reset {
margin-inline-start: auto;
}
.live-region {
display: flex;
align-items: center;
min-height: 3.5rem;
margin-top: 1rem;
padding: 1rem;
border: 2px dashed var(--border);
border-radius: 0.25rem;
background: var(--surface);
font-style: italic;
color: var(--text-secondary);
}
input:focus,
textarea:focus {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-color: var(--accent);
background: var(--background);
}
/* After :focus so the error state stays visible while the field is being corrected. */
:is(input, textarea)[aria-invalid="true"] {
border-color: var(--error);
box-shadow: inset 0 0 0 1px var(--error);
}
@media print {
:root {
color-scheme: light;
--text: #000;
--text-secondary: #333;
--background: #fff;
--surface: #fff;
--accent: #000;
--border: #999;
--border-control: #000;
--error: #000;
--success: #000;
}
.skip-link,
nav,
button,
.button,
.live-region {
display: none;
}
body {
display: block;
min-height: 0;
max-width: none;
padding: 0;
}
a {
color: inherit;
text-decoration: underline;
}
a[href^="http"]::after {
content: " (" attr(href) ")";
display: inline;
margin: 0;
font-size: 0.85em;
font-weight: normal;
}
.card {
border: none;
break-inside: avoid;
}
}
</style>
</head>
<body>
<noscript>
<style>
.theme-toggle, #announce, #decrease, #increase, #reset { display: none; }
</style>
<div style="padding: 1rem; background: var(--surface); border-bottom: 1px solid var(--border); color: var(--text-secondary);">
This page works best with JavaScript enabled for interactive features like theme switching and live announcements.
</div>
</noscript>
<a class="skip-link" href="#main">Skip to content</a>
<header id="top">
<div class="header-bar">
<h1>Accessible HTML5 Guide</h1>
<button id="theme-toggle" class="theme-toggle" type="button" aria-label="Switch to dark mode" title="Switch to dark mode" hidden>🌙</button>
</div>
<nav aria-label="Page sections">
<ul>
<li><a href="#features">Features</a></li>
<li><a href="#examples">Examples</a></li>
<li><a href="#accessibility">Accessibility</a></li>
<li><a href="#resources">Resources</a></li>
</ul>
</nav>
</header>
<main id="main" tabindex="-1">
<section>
<h2>Welcome to Accessible HTML5</h2>
<p>This is a well-structured, accessible HTML5 document demonstrating modern best practices for semantic markup and inclusive design.</p>
<p>
It uses semantic landmarks, a skip link for keyboard navigation,
and CSS that adapts to your light or dark mode preference.
</p>
<p>
Text and link colors meet WCAG AA contrast in both themes, type scales
with your browser's font size, and motion respects your system settings.
</p>
</section>
<section aria-labelledby="features">
<h2 id="features">Features</h2>
<ul>
<li>Semantic HTML structure with proper landmarks</li>
<li>Keyboard accessible navigation and focus management</li>
<li>Dark mode support with custom color schemes</li>
<li>Responsive typography using modern CSS</li>
<li>Print-friendly styles for better readability</li>
<li>WCAG AA contrast compliance in all color modes</li>
</ul>
</section>
<section aria-labelledby="examples">
<h2 id="examples">Examples</h2>
<article class="card">
<h3>Component Cards</h3>
<p>Card components are useful for organizing related content. They include a subtle border and padding for visual separation.</p>
<a class="button" href="#resources">Learn more<span class="visually-hidden"> about accessibility resources</span></a>
</article>
<article class="card">
<h3>Interactive Elements</h3>
<p>Buttons and links are fully accessible with clear focus states. All interactive elements include visible focus indicators and proper ARIA labels.</p>
<p><strong>Keyboard navigation:</strong> Use <kbd>Tab</kbd> to move forward, <kbd>Shift</kbd>+<kbd>Tab</kbd> to move backward, and <kbd>Enter</kbd> or <kbd>Space</kbd> to activate.</p>
</article>
<article class="card">
<h3>Accessible Form</h3>
<form id="contact-form">
<fieldset>
<legend>Contact Information</legend>
<div class="field">
<label for="email">Email <span class="form-hint" aria-hidden="true">(required)</span></label>
<input type="email" id="email" name="email" autocomplete="email" inputmode="email" spellcheck="false" autocapitalize="none" required aria-describedby="email-help email-error">
<span id="email-error" class="error-message" aria-live="polite"></span>
<small id="email-help" class="form-hint">For example, name@example.com</small>
</div>
<div class="field">
<label for="message">Message</label>
<textarea id="message" name="message" rows="4" maxlength="2000" aria-describedby="message-help"></textarea>
<small id="message-help" class="form-hint">Up to 2,000 characters. <span id="message-count">2,000</span> remaining.</small>
<span id="message-count-status" class="visually-hidden" role="status"></span>
</div>
</fieldset>
<button type="submit">Send message</button>
<p id="form-status" role="status"></p>
</form>
</article>
<article class="card">
<h3>Live Region Example</h3>
<p>Click the button to announce a message using a live region. This demonstrates how screen readers are notified of dynamic updates.</p>
<button id="announce" type="button">Announce message</button>
<div class="live-region" aria-live="polite" aria-atomic="true">
<span id="live-message"></span>
</div>
</article>
<article class="card">
<h3>Interactive Counter</h3>
<p>This counter demonstrates state management and ARIA live regions for accessible dynamic content. Screen readers will announce count changes.</p>
<div class="counter">
<button id="decrease" type="button" aria-label="Decrease count">−</button>
<output id="count" aria-live="polite">0</output>
<button id="increase" type="button" aria-label="Increase count">+</button>
<button id="reset" type="button" aria-label="Reset count">Reset</button>
</div>
<div class="live-region" aria-live="polite" aria-atomic="true">
<span id="count-message"></span>
</div>
</article>
</section>
<section aria-labelledby="accessibility">
<h2 id="accessibility">Accessibility Features</h2>
<article class="card">
<h3>Data Table Example</h3>
<p>Accessible tables have proper headers, captions, and semantic structure for screen readers.</p>
<table>
<caption>Browser compatibility for modern features</caption>
<thead>
<tr>
<th scope="col">Feature</th>
<th scope="col">Chrome</th>
<th scope="col">Firefox</th>
<th scope="col">Safari</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">CSS Grid</th>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
</tr>
<tr>
<th scope="row">CSS Variables</th>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
</tr>
<tr>
<th scope="row">Aspect Ratio</th>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
<td><span aria-hidden="true">✓</span><span class="visually-hidden">Supported</span></td>
</tr>
</tbody>
</table>
</article>
</section>
<section aria-labelledby="resources">
<h2 id="resources">Resources</h2>
<p>Want to learn more about web accessibility and semantic HTML? Check out these resources:</p>
<ul>
<li><a href="https://www.w3.org/WAI/WCAG22/quickref/" target="_blank" rel="noopener noreferrer">WCAG 2.2 Quick Reference<span class="visually-hidden"> (opens in new tab)</span></a></li>
<li><a href="https://html.spec.whatwg.org/" target="_blank" rel="noopener noreferrer">HTML Living Standard<span class="visually-hidden"> (opens in new tab)</span></a></li>
<li><a href="https://www.a11y-101.com/" target="_blank" rel="noopener noreferrer">A11y 101<span class="visually-hidden"> (opens in new tab)</span></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility" target="_blank" rel="noopener noreferrer">MDN Web Accessibility<span class="visually-hidden"> (opens in new tab)</span></a></li>
</ul>
</section>
</main>
<footer>
<nav aria-label="Footer links">
<ul>
<li><a href="#top">Back to top</a></li>
<li><a href="https://github.com" target="_blank" rel="noopener noreferrer">GitHub<span class="visually-hidden"> (opens in new tab)</span></a></li>
</ul>
</nav>
<p><small>© <span id="copyright-year">2026</span> K. Smith. All rights reserved.</small></p>
</footer>
<script>
const themeToggle = document.getElementById('theme-toggle');
const htmlElement = document.documentElement;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
const themeColors = { light: '#ffffff', dark: '#121212' };
const themeColorMetas = document.querySelectorAll('meta[name="theme-color"]');
themeColorMetas.forEach((meta) => {
meta.dataset.defaultContent = meta.content;
});
const updateThemeUI = () => {
const forced = htmlElement.dataset.theme;
const isDark = forced ? forced === 'dark' : prefersDark.matches;
const label = isDark ? 'Switch to light mode' : 'Switch to dark mode';
themeToggle.textContent = isDark ? '☀️' : '🌙';
themeToggle.setAttribute('aria-label', label);
themeToggle.title = label;
themeColorMetas.forEach((meta) => {
meta.content = forced ? themeColors[forced] : meta.dataset.defaultContent;
});
};
if (themeToggle) {
themeToggle.hidden = false;
updateThemeUI();
themeToggle.addEventListener('click', () => {
const systemTheme = prefersDark.matches ? 'dark' : 'light';
const current = htmlElement.dataset.theme || systemTheme;
const next = current === 'dark' ? 'light' : 'dark';
// Choosing the system theme clears the override so the page follows the OS again.
try {
if (next === systemTheme) {
delete htmlElement.dataset.theme;
localStorage.removeItem('theme');
} else {
htmlElement.dataset.theme = next;
localStorage.setItem('theme', next);
}
} catch {}
updateThemeUI();
});
prefersDark.addEventListener('change', () => {
if (!htmlElement.dataset.theme) {
updateThemeUI();
}
});
// Keep other open tabs in step with the choice made in this one.
window.addEventListener('storage', (event) => {
if (event.key !== null && event.key !== 'theme') {
return;
}
if (event.newValue === 'light' || event.newValue === 'dark') {
htmlElement.dataset.theme = event.newValue;
} else {
delete htmlElement.dataset.theme;
}
updateThemeUI();
});
}
const copyrightYear = document.getElementById('copyright-year');
if (copyrightYear) {
copyrightYear.textContent = String(new Date().getFullYear());
}
const messages = [
"This update was announced to screen readers!",
"Dynamic content updates work with aria-live regions.",
"Live regions are essential for accessible web apps.",
"Try this with a screen reader like NVDA or JAWS!"
];
let nextMessage = 0;
let count = 0;
const announceBtn = document.getElementById('announce');
const liveMessage = document.getElementById('live-message');
const contactForm = document.getElementById('contact-form');
const emailInput = document.getElementById('email');
const messageInput = document.getElementById('message');
const messageCount = document.getElementById('message-count');
const messageCountStatus = document.getElementById('message-count-status');
const formStatus = document.getElementById('form-status');
const countDisplay = document.getElementById('count');
const countMessage = document.getElementById('count-message');
const decreaseBtn = document.getElementById('decrease');
const increaseBtn = document.getElementById('increase');
const resetBtn = document.getElementById('reset');
const updateCountDisplay = () => {
countDisplay.textContent = String(count);
countMessage.textContent = `Count is ${count}.`;
};
if (countDisplay && countMessage) {
decreaseBtn?.addEventListener('click', () => {
count--;
updateCountDisplay();
});
increaseBtn?.addEventListener('click', () => {
count++;
updateCountDisplay();
});
resetBtn?.addEventListener('click', () => {
count = 0;
updateCountDisplay();
});
}
if (announceBtn && liveMessage) {
announceBtn.addEventListener('click', () => {
liveMessage.textContent = messages[nextMessage];
nextMessage = (nextMessage + 1) % messages.length;
});
}
if (contactForm) {
const emailError = document.getElementById('email-error');
const submitBtn = contactForm.querySelector('button[type="submit"]');
const isSending = () => submitBtn.getAttribute('aria-disabled') === 'true';
// Set here rather than in the markup so native validation still works without JavaScript.
contactForm.noValidate = true;
const setFormStatus = (message, state) => {
formStatus.textContent = message;
if (state) {
formStatus.dataset.state = state;
} else {
delete formStatus.dataset.state;
}
};
const countThresholds = [0, 10, 25, 50, 100];
let lastThreshold;
const updateMessageCount = () => {
if (messageInput && messageCount) {
const remaining = messageInput.maxLength - messageInput.value.length;
// Match the page language rather than the browser locale.
messageCount.textContent = remaining.toLocaleString(htmlElement.lang);
// Announce only when a threshold is crossed, not on every keystroke.
const threshold = countThresholds.find((limit) => remaining <= limit);
if (messageCountStatus && threshold !== lastThreshold) {
lastThreshold = threshold;
messageCountStatus.textContent = threshold === undefined
? ''
: `${remaining.toLocaleString(htmlElement.lang)} characters remaining.`;
}
}
};
messageInput?.addEventListener('input', updateMessageCount);
// Browsers may restore field values on back/forward navigation.
window.addEventListener('pageshow', updateMessageCount);
updateMessageCount();
const setEmailError = (message) => {
emailError.textContent = message;
if (message) {
emailInput.setAttribute('aria-invalid', 'true');
emailInput.setAttribute('aria-describedby', 'email-error email-help');
} else {
emailInput.removeAttribute('aria-invalid');
emailInput.setAttribute('aria-describedby', 'email-help');
}
};
const validateEmail = () => {
let message = '';
if (emailInput.validity.valueMissing) {
message = 'Email address is required for us to respond.';
} else if (emailInput.validity.typeMismatch) {
message = 'Please enter a valid email address, such as name@example.com.';
} else if (!emailInput.validity.valid) {
message = 'Please enter a valid email address.';
}
setEmailError(message);
return !message;
};
emailInput.addEventListener('blur', () => {
if (emailInput.value || emailInput.hasAttribute('aria-invalid')) {
validateEmail();
}
});
emailInput.addEventListener('input', () => {
if (emailInput.hasAttribute('aria-invalid')) {
validateEmail();
}
});
contactForm.addEventListener('input', () => {
if (!isSending()) {
setFormStatus('');
}
});
contactForm.addEventListener('submit', async (event) => {
event.preventDefault();
if (isSending()) {
return;
}
setFormStatus('');
if (!validateEmail()) {
setFormStatus('Please correct the errors above before submitting.', 'error');
emailInput.focus();
return;
}
const originalText = submitBtn.textContent;
submitBtn.setAttribute('aria-disabled', 'true');
submitBtn.setAttribute('aria-busy', 'true');
submitBtn.textContent = 'Sending…';
setFormStatus('Sending your message…', 'pending');
emailInput.readOnly = true;
if (messageInput) messageInput.readOnly = true;
await new Promise(resolve => setTimeout(resolve, 800));
emailInput.readOnly = false;
if (messageInput) messageInput.readOnly = false;
contactForm.reset();
setEmailError('');
updateMessageCount();
submitBtn.removeAttribute('aria-disabled');
submitBtn.removeAttribute('aria-busy');
submitBtn.textContent = originalText;
setFormStatus('Success! Thanks for your message. This is a demo, so nothing was sent.');
emailInput.focus();
});
}
</script>
</body>
</html>
what changed from step 059
+33 / -13 lines
th, td {- padding: 0.5rem;+ padding: 0.75rem; text-align: center; border-bottom: 1px solid var(--border);⋯ line 348 thead th { border-bottom-width: 2px;+ font-weight: 700;+ background: var(--surface); } tbody tr:last-child > * {⋯ line 443 background: color-mix(in srgb, var(--success) 10%, transparent); color: var(--success);- border: 1px solid var(--success);+ border: 2px solid var(--success); animation: slideDown 0.3s ease;+ font-weight: 500; } #form-status[data-state="pending"] {⋯ line 471 color: var(--error); border-color: var(--error);+ border-width: 2px;+ font-weight: 500; } .form-hint {⋯ line 499 } .counter output {- min-width: 3ch;- font-size: 1.5rem;- font-weight: 600;+ min-width: 4ch;+ font-size: 1.75rem;+ font-weight: 700; font-variant-numeric: tabular-nums; color: var(--accent); text-align: center;+ padding: 0.5rem;+ background: var(--surface);+ border-radius: 0.25rem;+ border: 2px solid var(--border); } .counter #reset {⋯ line 519 margin-top: 1rem; padding: 1rem;- border: 1px dashed var(--border);+ border: 2px dashed var(--border); border-radius: 0.25rem;+ background: var(--surface);+ font-style: italic;+ color: var(--text-secondary); } input:focus,⋯ line 582 </head> <body>+ <noscript>+ <style>+ .theme-toggle, #announce, #decrease, #increase, #reset { display: none; }+ </style>+ <div style="padding: 1rem; background: var(--surface); border-bottom: 1px solid var(--border); color: var(--text-secondary);">+ This page works best with JavaScript enabled for interactive features like theme switching and live announcements.+ </div>+ </noscript> <a class="skip-link" href="#main">Skip to content</a> <header id="top">⋯ line 679 <article class="card"> <h3>Interactive Counter</h3>- <p>This counter demonstrates state management and ARIA live regions for accessible dynamic content.</p>+ <p>This counter demonstrates state management and ARIA live regions for accessible dynamic content. Screen readers will announce count changes.</p> <div class="counter">- <button id="decrease" type="button" aria-label="Decrease count by 1">−</button>- <output id="count" aria-live="off">0</output>- <button id="increase" type="button" aria-label="Increase count by 1">+</button>- <button id="reset" type="button" aria-label="Reset count to zero">Reset</button>+ <button id="decrease" type="button" aria-label="Decrease count">−</button>+ <output id="count" aria-live="polite">0</output>+ <button id="increase" type="button" aria-label="Increase count">+</button>+ <button id="reset" type="button" aria-label="Reset count">Reset</button> </div> <div class="live-region" aria-live="polite" aria-atomic="true">⋯ line 926 let message = ''; if (emailInput.validity.valueMissing) {- message = 'Email address is required.';+ message = 'Email address is required for us to respond.'; } else if (emailInput.validity.typeMismatch) {- message = 'Please enter a valid email address (e.g., name@example.com).';+ message = 'Please enter a valid email address, such as name@example.com.'; } else if (!emailInput.validity.valid) { message = 'Please enter a valid email address.';
What the model said
Perfect! I've successfully improved the HTML file with several meaningful enhancements: ## Improvements Made 1. **JavaScript Fallback** - Added a `<noscript>` section that displays a helpful message when JavaScript is disabled, hiding interactive elements and explaining what requires JS. 2. **Enhanced Form Feedback** - Improved form status messages with better visual emphasis (thicker borders, bold text) and clearer validation messages that are more user-friendly. 3. **Better Counter Accessibility** - Changed the counter output to use `aria-live="polite"` so screen readers announce changes dynamically. Streamlined ARIA labels on counter buttons. 4. **Counter Display Polish** - Enhanced the counter display with a styled box (border, padding, background) to make it stand out more visually. 5. **Improved Table Styling** - Increased padding for better readability and added background highlighting to table headers. 6. **Live Region Enhancement** - Made the live region more visually distinct with a thicker border, background, and italic text styling. 7. **Better Email Validation** - Updated validation messages to be more contextual and helpful (e.g., "Email address is required for us to respond"). All changes maintain the high accessibility standards and modern best practices already present in the document. The file is ready to use and test in a browser.