1939 lines
65 KiB
HTML
1939 lines
65 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
|
<title>The Complete Native HTML Dominance Guide</title>
|
|
<style>
|
|
:root {
|
|
--color-bg: #ffffff;
|
|
--color-card: #f8f9fa;
|
|
--color-primary: #0066cc;
|
|
--color-primary-hover: #0052a3;
|
|
--color-danger: #dc3545;
|
|
--color-warning: #856404;
|
|
--color-warning-bg: #fff3cd;
|
|
--color-content-bg: #f1f3f4;
|
|
--color-border: #dee2e6;
|
|
--color-text: #212529;
|
|
--color-text-muted: #6c757d;
|
|
--color-code-bg: #f8f9fa;
|
|
--color-code-text: #0066cc;
|
|
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
--radius: 8px;
|
|
--focus-outline: 2px solid #0066cc;
|
|
--focus-offset: 2px;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
line-height: 1.6;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.hero {
|
|
text-align: center;
|
|
margin-bottom: 4rem;
|
|
padding: 3rem;
|
|
background: var(--color-card);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
border: 1px solid var(--color-border);
|
|
}
|
|
|
|
.hero:focus-within {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
.hero h1 {
|
|
font-size: clamp(2rem, 5vw, 3.5rem);
|
|
margin-bottom: 1.5rem;
|
|
color: var(--color-primary);
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.hero .subtitle {
|
|
font-size: 1.125rem;
|
|
color: var(--color-text-muted);
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.comparison-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 2rem;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.section {
|
|
margin: 4rem 0;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 2rem;
|
|
margin-bottom: 2rem;
|
|
padding: 1.5rem 0;
|
|
text-align: center;
|
|
color: var(--color-primary);
|
|
position: relative;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.section-title::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 60px;
|
|
height: 2px;
|
|
background: var(--color-primary);
|
|
}
|
|
|
|
.column {
|
|
background: var(--color-card);
|
|
padding: 2rem;
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
border: 1px solid var(--color-border);
|
|
transition: box-shadow 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.column:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.column:focus-within {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
.column h3 {
|
|
color: var(--color-text);
|
|
border-bottom: 1px solid var(--color-border);
|
|
padding-bottom: 1rem;
|
|
margin-bottom: 2rem;
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.bad {
|
|
border-left: 3px solid var(--color-danger);
|
|
}
|
|
|
|
.bad h3::before {
|
|
content: '⚠️ ';
|
|
}
|
|
|
|
.good {
|
|
border-left: 3px solid var(--color-primary);
|
|
}
|
|
|
|
.good h3::before {
|
|
content: '✅ ';
|
|
}
|
|
|
|
.accessibility-note {
|
|
background: var(--color-warning-bg);
|
|
border: 1px solid #ffc107;
|
|
color: var(--color-warning);
|
|
padding: 1rem;
|
|
border-radius: var(--radius);
|
|
margin: 1.5rem 0;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.code-block {
|
|
/*display: none;*/
|
|
background: var(--color-code-bg);
|
|
color: var(--color-code-text);
|
|
padding: 2rem 1.5rem 1rem;
|
|
border-radius: var(--radius);
|
|
overflow-x: auto;
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
font-size: 0.875rem;
|
|
margin: 1.5rem 0;
|
|
border: 1px solid var(--color-border);
|
|
position: relative;
|
|
}
|
|
|
|
.code-block:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
.code-block::before {
|
|
content: 'CODE';
|
|
position: absolute;
|
|
top: -6px;
|
|
right: 15px;
|
|
background: var(--color-primary);
|
|
color: white;
|
|
padding: 10px 8px 0;
|
|
font-size: 0.7rem;
|
|
border-radius: 4px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pros-cons {
|
|
margin: 1.5rem 0;
|
|
}
|
|
|
|
.pros-cons h4 {
|
|
margin-bottom: 1rem;
|
|
color: var(--color-text);
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pros-cons ul {
|
|
list-style: none;
|
|
padding-left: 0;
|
|
}
|
|
|
|
.pros-cons li {
|
|
padding: 0.5rem 0;
|
|
padding-left: 1.5rem;
|
|
position: relative;
|
|
}
|
|
|
|
.pros li::before {
|
|
content: '✓';
|
|
position: absolute;
|
|
left: 0;
|
|
color: var(--color-primary);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.cons li::before {
|
|
content: '✗';
|
|
position: absolute;
|
|
left: 0;
|
|
color: var(--color-danger);
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Interactive Elements */
|
|
.js-collapsible {
|
|
background: var(--color-danger);
|
|
color: white;
|
|
cursor: pointer;
|
|
padding: 12px 16px;
|
|
border: none;
|
|
text-align: left;
|
|
outline: none;
|
|
font-size: 1rem;
|
|
border-radius: var(--radius);
|
|
margin-bottom: 1rem;
|
|
width: 100%;
|
|
transition: all 0.2s ease;
|
|
font-family: inherit;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.js-collapsible:hover {
|
|
background: #c82333;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.js-collapsible:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
.js-content {
|
|
padding: 0;
|
|
display: none;
|
|
background: var(--color-content-bg);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 0 0 var(--radius) var(--radius);
|
|
margin-bottom: 1rem;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.js-content.active {
|
|
display: block;
|
|
padding: 16px;
|
|
}
|
|
|
|
details {
|
|
background: var(--color-card);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius);
|
|
margin-bottom: 1rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
details:focus-within {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
summary {
|
|
background: var(--color-primary);
|
|
color: white;
|
|
padding: 12px 16px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
list-style: none;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
summary:hover {
|
|
background: var(--color-primary-hover);
|
|
}
|
|
|
|
summary:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
summary::before {
|
|
content: '▶';
|
|
margin-right: 8px;
|
|
transition: transform 0.2s ease;
|
|
display: inline-block;
|
|
}
|
|
|
|
details[open] summary::before {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
details > div {
|
|
padding: 16px;
|
|
background: var(--color-content-bg);
|
|
}
|
|
|
|
/* Modal Styles */
|
|
.js-modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
z-index: 1000;
|
|
}
|
|
|
|
.js-modal-overlay.active {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.js-modal-content {
|
|
background: var(--color-bg);
|
|
padding: 2rem;
|
|
border-radius: var(--radius);
|
|
max-width: 500px;
|
|
width: 90%;
|
|
border: 1px solid var(--color-border);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.js-modal-content:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
dialog {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius);
|
|
padding: 2rem;
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
box-shadow: var(--shadow);
|
|
max-width: 500px;
|
|
width: 90%;
|
|
}
|
|
|
|
dialog:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
dialog::backdrop {
|
|
background: rgba(0, 0, 0, 0.6);
|
|
}
|
|
|
|
/* Form Styles */
|
|
.form-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
font-weight: 500;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.form-group input, .form-group textarea, .form-group select {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius);
|
|
font-size: 1rem;
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
transition: all 0.2s ease;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: -2px;
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.form-group input:invalid {
|
|
border-color: var(--color-danger);
|
|
}
|
|
|
|
.form-group input:valid {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.js-error {
|
|
color: var(--color-danger);
|
|
font-size: 0.875rem;
|
|
margin-top: 0.5rem;
|
|
display: none;
|
|
}
|
|
|
|
.js-error.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Progress Styles */
|
|
.js-progress-container {
|
|
width: 100%;
|
|
background: var(--color-border);
|
|
border-radius: 4px;
|
|
height: 20px;
|
|
margin: 1.5rem 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.js-progress-bar {
|
|
height: 100%;
|
|
background: var(--color-danger);
|
|
width: 0;
|
|
transition: width 0.3s ease;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
progress {
|
|
width: 100%;
|
|
height: 20px;
|
|
margin: 1.5rem 0;
|
|
border-radius: 4px;
|
|
border: none;
|
|
background: var(--color-border);
|
|
}
|
|
|
|
progress:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
progress::-webkit-progress-bar {
|
|
background-color: var(--color-border);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
progress::-webkit-progress-value {
|
|
background: var(--color-primary);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
progress::-moz-progress-bar {
|
|
background: var(--color-primary);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Button Styles */
|
|
button {
|
|
background: var(--color-primary);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 16px;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
margin: 0.5rem 0.5rem 0.5rem 0;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
font-family: inherit;
|
|
}
|
|
|
|
button:hover {
|
|
background: var(--color-primary-hover);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
button:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
button:disabled {
|
|
background: var(--color-text-muted);
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
/* Component Styles */
|
|
.datalist-container {
|
|
position: relative;
|
|
}
|
|
|
|
input[list] {
|
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23666' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
|
|
background-position: right 12px center;
|
|
background-repeat: no-repeat;
|
|
background-size: 16px;
|
|
padding-right: 36px;
|
|
}
|
|
|
|
.slider-container {
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
input[type="range"] {
|
|
width: 100%;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
background: var(--color-border);
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
input[type="range"]:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
input[type="range"]::-webkit-slider-thumb {
|
|
appearance: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: var(--color-primary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
input[type="range"]::-moz-range-thumb {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: var(--color-primary);
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
.color-input {
|
|
width: 50px;
|
|
height: 32px;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.color-input:focus {
|
|
outline: var(--focus-outline);
|
|
outline-offset: var(--focus-offset);
|
|
}
|
|
|
|
.summary-box {
|
|
margin-top: 4rem;
|
|
padding: 2rem;
|
|
background: var(--color-card);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
border: 1px solid var(--color-border);
|
|
text-align: center;
|
|
}
|
|
|
|
.summary-box h3 {
|
|
font-size: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
color: var(--color-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.zen-quote {
|
|
font-style: italic;
|
|
color: var(--color-text-muted);
|
|
font-size: 1rem;
|
|
margin-top: 1.5rem;
|
|
border-left: 3px solid var(--color-primary);
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.comparison-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
body {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.hero {
|
|
padding: 2rem;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.75rem;
|
|
}
|
|
}
|
|
|
|
/* Performance indicators */
|
|
.perf-indicator {
|
|
display: inline-block;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.perf-fast {
|
|
background: rgba(0, 102, 204, 0.1);
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.perf-slow {
|
|
background: rgba(220, 53, 69, 0.1);
|
|
color: var(--color-danger);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="hero">
|
|
<h1>🚀 No JavaScript, No cry</h1>
|
|
<p class="subtitle">Stop fighting the platform. HTML already solved 90% of your UI problems. This guide exposes the
|
|
absurdity of JavaScript wheel-reinvention and shows you the path to enlightened web development through semantic
|
|
markup and progressive enhancement.</p>
|
|
</div>
|
|
|
|
<!-- COLLAPSIBLES -->
|
|
<div class="section">
|
|
<h2 class="section-title">🎯 Collapsible Content</h2>
|
|
<div class="comparison-grid">
|
|
<div class="column bad">
|
|
<h3>JavaScript Nightmare <span class="perf-indicator perf-slow">SLOW</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>⚠️ Accessibility Disaster:</strong> No ARIA support, keyboard navigation broken, screen readers
|
|
confused, focus management missing. You're essentially telling assistive technology users to get lost.
|
|
</div>
|
|
|
|
<button class="js-collapsible" onclick="toggleContent('content1')">
|
|
What is Web Accessibility? (Click me if you dare)
|
|
</button>
|
|
<div class="js-content" id="content1">
|
|
Web accessibility ensures that websites and digital tools are usable by people with disabilities. This includes
|
|
visual, auditory, motor, and cognitive impairments. Too bad this JavaScript implementation ignores all of that.
|
|
</div>
|
|
|
|
<button class="js-collapsible" onclick="toggleContent('content2')">
|
|
Why WCAG Actually Matters
|
|
</button>
|
|
<div class="js-content" id="content2">
|
|
WCAG provides the framework for making web content accessible. It's not just legal compliance - it's about not
|
|
being a terrible human being who excludes people from the web.
|
|
</div>
|
|
|
|
<div class="code-block">
|
|
<!-- The horror show begins -->
|
|
<button onclick="toggleContent('id')">Click</button>
|
|
<div id="content" style="display:none">Hidden content</div>
|
|
|
|
<script>
|
|
// Because apparently HTML isn't good enough
|
|
function toggleContent(id) {
|
|
const content = document.getElementById(id);
|
|
content.style.display =
|
|
content.style.display === 'none' ? 'block' : 'none';
|
|
// Missing: ARIA states, keyboard nav, focus management
|
|
// Basically everything that makes it accessible
|
|
}
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="cons">❌ Why This Sucks:</h4>
|
|
<ul>
|
|
<li>Zero semantic meaning - screen readers see meaningless divs</li>
|
|
<li>No ARIA attributes - assistive tech has no clue what's happening</li>
|
|
<li>Keyboard navigation requires custom implementation</li>
|
|
<li>JavaScript dependency - breaks without JS</li>
|
|
<li>SEO nightmare - hidden content not properly indexed</li>
|
|
<li>State management gets complex with multiple sections</li>
|
|
<li>Performance overhead for simple interactions</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column good">
|
|
<h3>Native HTML Zen <span class="perf-indicator perf-fast">FAST</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>🎯 Accessibility Nirvana:</strong> Built-in ARIA, keyboard support, semantic meaning, screen reader
|
|
friendly, works without JavaScript. This is what inclusive design looks like.
|
|
</div>
|
|
|
|
<details>
|
|
<summary>What is Web Accessibility?</summary>
|
|
<div>
|
|
Web accessibility ensures that websites and digital tools are usable by people with disabilities. This
|
|
includes visual, auditory, motor, and cognitive impairments. The native HTML approach provides this
|
|
functionality with zero JavaScript - because the web platform actually cares about inclusion.
|
|
</div>
|
|
</details>
|
|
|
|
<details>
|
|
<summary>Why WCAG Actually Matters</summary>
|
|
<div>
|
|
WCAG provides the framework for making web content accessible. It's not just legal compliance - it's about
|
|
building a web that serves everyone. Native elements follow these guidelines by default because they were
|
|
designed by people who understand accessibility.
|
|
</div>
|
|
</details>
|
|
|
|
<details>
|
|
<summary>Progressive Enhancement Philosophy</summary>
|
|
<div>
|
|
Start with functional HTML, enhance with CSS, sprinkle JavaScript only where needed. This approach ensures
|
|
your content works for everyone, regardless of their device, connection, or abilities. It's not just good
|
|
engineering - it's good humanity.
|
|
</div>
|
|
</details>
|
|
|
|
<div class="code-block">
|
|
<!-- The elegance of simplicity -->
|
|
<details>
|
|
<summary>Click to expand</summary>
|
|
<div>
|
|
Content that's hidden by default,
|
|
but accessible to screen readers,
|
|
searchable by search engines,
|
|
and works without JavaScript.
|
|
|
|
This is what the web was meant to be.
|
|
</div>
|
|
</details>
|
|
|
|
<!-- That's literally it. No JavaScript required. -->
|
|
<!-- The browser handles everything: ARIA, keyboard nav, -->
|
|
<!-- focus management, state persistence. -->
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="pros">✅ Why This Rules:</h4>
|
|
<ul>
|
|
<li>Semantic HTML5 element with inherent meaning</li>
|
|
<li>Built-in ARIA support - screen readers understand it</li>
|
|
<li>Keyboard accessible out of the box (Space/Enter)</li>
|
|
<li>Works without JavaScript - progressive enhancement</li>
|
|
<li>SEO friendly - search engines can crawl collapsed content</li>
|
|
<li>Browser handles state management and animations</li>
|
|
<li>Consistent UX across all browsers and platforms</li>
|
|
<li>Zero maintenance overhead - it just works</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODALS -->
|
|
<div class="section">
|
|
<h2 class="section-title">🪟 Modal Dialogs</h2>
|
|
<div class="comparison-grid">
|
|
<div class="column bad">
|
|
<h3>JavaScript Chaos <span class="perf-indicator perf-slow">JANKY</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>⚠️ Accessibility Nightmare:</strong> Focus trapping broken, ESC key ignored, backdrop clicks
|
|
inconsistent, ARIA roles missing. Your users with disabilities are stuck in modal hell.
|
|
</div>
|
|
|
|
<button onclick="openJSModal()">Open JS Modal (prepare for suffering)</button>
|
|
|
|
<div class="js-modal-overlay" id="jsModal">
|
|
<div class="js-modal-content">
|
|
<h4>JavaScript Modal of Doom</h4>
|
|
<p>This modal requires hundreds of lines of custom JavaScript for focus management, ESC key handling, backdrop
|
|
clicks, and ARIA attributes. And it still probably has bugs.</p>
|
|
<button onclick="closeJSModal()">Escape This Hell</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="code-block">
|
|
<!-- The complexity monster -->
|
|
<div class="modal-overlay" id="myModal">
|
|
<div class="modal-content">
|
|
<h4>Modal Title</h4>
|
|
<p>Modal content</p>
|
|
<button onclick="closeModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Welcome to callback hell
|
|
function openModal() {
|
|
const modal = document.getElementById('myModal');
|
|
modal.classList.add('active');
|
|
|
|
// Manual focus trapping
|
|
trapFocus(modal);
|
|
|
|
// ESC key handling
|
|
document.addEventListener('keydown', handleEscape);
|
|
|
|
// Backdrop click handling
|
|
modal.addEventListener('click', handleBackdrop);
|
|
|
|
// Disable body scroll
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
// ARIA attributes
|
|
modal.setAttribute('aria-hidden', 'false');
|
|
modal.setAttribute('role', 'dialog');
|
|
|
|
// Focus management
|
|
modal.querySelector('button').focus();
|
|
}
|
|
|
|
// And this is just the opening logic...
|
|
// Closing, focus restoration, cleanup - hundreds more lines
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="cons">❌ The Suffering:</h4>
|
|
<ul>
|
|
<li>Focus trapping requires complex event management</li>
|
|
<li>ESC key handling needs custom implementation</li>
|
|
<li>Backdrop click behavior is inconsistent across browsers</li>
|
|
<li>Body scroll locking breaks on mobile</li>
|
|
<li>ARIA attributes must be managed manually</li>
|
|
<li>Focus restoration is fragile and bug-prone</li>
|
|
<li>Event cleanup leads to memory leaks</li>
|
|
<li>Testing requires mocking dozens of edge cases</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column good">
|
|
<h3>Native Dialog Enlightenment <span class="perf-indicator perf-fast">PERFECT</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>🎯 Accessibility Perfection:</strong> Built-in focus trapping, ESC key support, backdrop clicks, ARIA
|
|
roles, screen reader announcements. This is what happens when browsers do the heavy lifting.
|
|
</div>
|
|
|
|
<button onclick="document.getElementById('nativeDialog').showModal()">
|
|
Open Native Dialog (experience bliss)
|
|
</button>
|
|
|
|
<dialog id="nativeDialog">
|
|
<h4>Native Dialog Zen</h4>
|
|
<p>This dialog provides built-in focus trapping, ESC key support, backdrop clicks, proper ARIA semantics, body
|
|
scroll locking, and focus restoration. All with zero custom JavaScript.</p>
|
|
<p><strong>The browser is your friend.</strong> Stop fighting it.</p>
|
|
<button onclick="document.getElementById('nativeDialog').close()">
|
|
Close (via built-in method)
|
|
</button>
|
|
</dialog>
|
|
|
|
<div class="code-block">
|
|
<!-- The path to enlightenment -->
|
|
<dialog id="myDialog">
|
|
<h4>Dialog Title</h4>
|
|
<p>Dialog content that just works</p>
|
|
<button onclick="this.closest('dialog').close()">
|
|
Close
|
|
</button>
|
|
</dialog>
|
|
|
|
<script>
|
|
// Modal dialog
|
|
document.getElementById('myDialog').showModal();
|
|
|
|
// Non-modal dialog (doesn't block interaction with page)
|
|
document.getElementById('myDialog').show();
|
|
|
|
// That's it. The browser handles:
|
|
// - Focus trapping
|
|
// - ESC key handling
|
|
// - Backdrop click support
|
|
// - ARIA roles and states
|
|
// - Body scroll locking
|
|
// - Focus restoration
|
|
// - Screen reader announcements
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="pros">✅ The Enlightenment:</h4>
|
|
<ul>
|
|
<li>Built-in focus trapping - no event management needed</li>
|
|
<li>ESC key support works automatically</li>
|
|
<li>Backdrop clicks handled by browser</li>
|
|
<li>Body scroll locking just works</li>
|
|
<li>ARIA roles and states managed automatically</li>
|
|
<li>Focus restoration is bulletproof</li>
|
|
<li>Zero memory leaks or event cleanup</li>
|
|
<li>Works consistently across all browsers</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FORM VALIDATION -->
|
|
<div class="section">
|
|
<h2 class="section-title">📝 Form Validation</h2>
|
|
<div class="comparison-grid">
|
|
<div class="column bad">
|
|
<h3>JavaScript Validation Hell <span class="perf-indicator perf-slow">BUGGY</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>⚠️ Accessibility Failure:</strong> Custom error messages not properly announced, validation timing
|
|
issues, focus management broken, inconsistent UX across form fields.
|
|
</div>
|
|
|
|
<form onsubmit="return validateJSForm(event)">
|
|
<div class="form-group">
|
|
<label for="js-email">Email Address</label>
|
|
<input id="js-email" name="email" type="text">
|
|
<div class="js-error" id="email-error">Please enter a valid email</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="js-password">Password</label>
|
|
<input id="js-password" name="password" type="password">
|
|
<div class="js-error" id="password-error">Password must be at least 8 characters</div>
|
|
</div>
|
|
|
|
<button type="submit">Submit (pray it works)</button>
|
|
</form>
|
|
|
|
<div class="code-block">
|
|
<!-- The validation nightmare -->
|
|
<form onsubmit="return validateForm(event)">
|
|
<input type="text" id="email" name="email">
|
|
<div class="error" id="email-error"></div>
|
|
|
|
<input type="password" id="password">
|
|
<div class="error" id="password-error"></div>
|
|
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
|
|
<script>
|
|
function validateForm(event) {
|
|
event.preventDefault();
|
|
let isValid = true;
|
|
|
|
// Email validation
|
|
const email = document.getElementById('email').value;
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
const emailError = document.getElementById('email-error');
|
|
|
|
if (!emailRegex.test(email)) {
|
|
emailError.textContent = 'Please enter a valid email';
|
|
emailError.classList.add('active');
|
|
isValid = false;
|
|
} else {
|
|
emailError.classList.remove('active');
|
|
}
|
|
|
|
// Password validation
|
|
const password = document.getElementById('password').value;
|
|
const passwordError = document.getElementById('password-error');
|
|
|
|
if (password.length < 8) {
|
|
passwordError.textContent = 'Password must be at least 8 characters';
|
|
passwordError.classList.add('active');
|
|
isValid = false;
|
|
} else {
|
|
passwordError.classList.remove('active');
|
|
}
|
|
|
|
// Focus management
|
|
if (!isValid) {
|
|
const firstError = document.querySelector('.error.active');
|
|
if (firstError) {
|
|
firstError.previousElementSibling.focus();
|
|
}
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
// Real-time validation (more complexity)
|
|
document.getElementById('email').addEventListener('blur', validateEmail);
|
|
document.getElementById('password').addEventListener('input', validatePassword);
|
|
// ... hundreds more lines of validation logic
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="cons">❌ The Validation Suffering:</h4>
|
|
<ul>
|
|
<li>Custom regex patterns that miss edge cases</li>
|
|
<li>Manual error message management and display</li>
|
|
<li>Event listener management becomes complex</li>
|
|
<li>Inconsistent validation timing across fields</li>
|
|
<li>Screen readers may not announce custom errors</li>
|
|
<li>Form submission logic gets tangled with validation</li>
|
|
<li>Different validation rules for client vs server</li>
|
|
<li>Styling validation states requires CSS gymnastics</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column good">
|
|
<h3>Native HTML5 Validation Zen <span class="perf-indicator perf-fast">SOLID</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>🎯 Accessibility Excellence:</strong> Built-in error announcements, proper ARIA attributes, consistent
|
|
validation timing, native browser UX that users already understand.
|
|
</div>
|
|
|
|
<form>
|
|
<div class="form-group">
|
|
<label for="native-email-wcag">Email Address</label>
|
|
<input id="native-email-wcag" name="email" placeholder="user@example.com" required
|
|
type="email">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-password">Password</label>
|
|
<input id="native-password" minlength="8" name="password"
|
|
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$" required
|
|
title="Must contain at least 8 characters with uppercase, lowercase, and number"
|
|
type="password">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-url">Website URL</label>
|
|
<input id="native-url" name="website" placeholder="https://example.com"
|
|
type="url">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-phone">Phone Number</label>
|
|
<input id="native-phone" name="phone" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
|
|
placeholder="123-456-7890"
|
|
type="tel">
|
|
</div>
|
|
|
|
<button type="submit">Submit (it just works)</button>
|
|
</form>
|
|
|
|
<div class="code-block">
|
|
<!-- The path to validation enlightenment -->
|
|
<form>
|
|
<!-- Email validation built-in -->
|
|
<input type="email" name="email" required>
|
|
|
|
<!-- Password with length and pattern -->
|
|
<input type="password" name="password"
|
|
required minlength="8"
|
|
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
|
|
title="Must contain uppercase, lowercase, and number">
|
|
|
|
<!-- URL validation -->
|
|
<input type="url" name="website">
|
|
|
|
<!-- Phone number with pattern -->
|
|
<input type="tel" name="phone"
|
|
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}">
|
|
|
|
<!-- Number with range -->
|
|
<input type="number" name="age" min="18" max="120">
|
|
|
|
<!-- Custom validation message -->
|
|
<input type="text" name="username"
|
|
pattern="[a-zA-Z0-9_]{3,20}"
|
|
title="3-20 characters, letters, numbers, underscore only">
|
|
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
|
|
<!-- Optional: Custom validation with Constraint API -->
|
|
<script>
|
|
// Only if you need custom logic beyond HTML5
|
|
const input = document.querySelector('[name="username"]');
|
|
input.addEventListener('input', function() {
|
|
if (this.value.includes('admin')) {
|
|
this.setCustomValidity('Username cannot contain "admin"');
|
|
} else {
|
|
this.setCustomValidity('');
|
|
}
|
|
});
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="pros">✅ The Validation Enlightenment:</h4>
|
|
<ul>
|
|
<li>Built-in validation for email, URL, number, tel, date types</li>
|
|
<li>Pattern attribute for custom regex validation</li>
|
|
<li>Min/max length and value constraints</li>
|
|
<li>Required attribute for mandatory fields</li>
|
|
<li>Automatic error messages in user's language</li>
|
|
<li>:valid and :invalid CSS pseudo-classes</li>
|
|
<li>Form won't submit until all fields are valid</li>
|
|
<li>Constraint Validation API for custom logic</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PROGRESS INDICATORS -->
|
|
<div class="section">
|
|
<h2 class="section-title">📊 Progress Indicators</h2>
|
|
<div class="comparison-grid">
|
|
<div class="column bad">
|
|
<h3>JavaScript Progress Chaos <span class="perf-indicator perf-slow">JANKY</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>⚠️ Accessibility Disaster:</strong> No semantic meaning, screen readers can't announce progress, missing
|
|
ARIA labels, custom implementation breaks assistive technology.
|
|
</div>
|
|
|
|
<div class="js-progress-container">
|
|
<div class="js-progress-bar" id="jsProgress"></div>
|
|
</div>
|
|
<p>JavaScript Progress: <span id="jsProgressText">0%</span></p>
|
|
<button onclick="startJSProgress()">Start JS Progress (watch it struggle)</button>
|
|
|
|
<div class="code-block">
|
|
<!-- The overcomplicated approach -->
|
|
<div class="progress-container">
|
|
<div class="progress-bar" id="progressBar"></div>
|
|
</div>
|
|
<div class="progress-text" id="progressText">0%</div>
|
|
|
|
<style>
|
|
.progress-container {
|
|
width: 100%;
|
|
height: 20px;
|
|
background: #ddd;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
|
width: 0%;
|
|
transition: width 0.3s ease;
|
|
border-radius: 10px;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function updateProgress(percentage) {
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressText = document.getElementById('progressText');
|
|
|
|
// Manual width calculation
|
|
progressBar.style.width = percentage + '%';
|
|
progressText.textContent = percentage + '%';
|
|
|
|
// Manual ARIA updates (often forgotten)
|
|
progressBar.setAttribute('aria-valuenow', percentage);
|
|
progressBar.setAttribute('aria-valuetext', percentage + ' percent');
|
|
|
|
// Color changes based on progress
|
|
if (percentage < 30) {
|
|
progressBar.style.background = '#f44336';
|
|
} else if (percentage < 70) {
|
|
progressBar.style.background = '#ff9800';
|
|
} else {
|
|
progressBar.style.background = '#4CAF50';
|
|
}
|
|
}
|
|
|
|
// File upload progress example
|
|
function uploadFile(file) {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.upload.addEventListener('progress', function(e) {
|
|
if (e.lengthComputable) {
|
|
const percentage = (e.loaded / e.total) * 100;
|
|
updateProgress(Math.round(percentage));
|
|
}
|
|
});
|
|
// ... more upload logic
|
|
}
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="cons">❌ Progress Bar Problems:</h4>
|
|
<ul>
|
|
<li>No semantic meaning - just styled divs</li>
|
|
<li>ARIA attributes must be managed manually</li>
|
|
<li>Screen readers can't announce progress changes</li>
|
|
<li>Custom styling requires complex CSS</li>
|
|
<li>Animation performance varies across browsers</li>
|
|
<li>Value calculations prone to rounding errors</li>
|
|
<li>Indeterminate progress requires additional logic</li>
|
|
<li>Color coding needs manual implementation</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column good">
|
|
<h3>Native Progress Perfection <span class="perf-indicator perf-fast">SMOOTH</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>🎯 Accessibility Paradise:</strong> Semantic HTML element, built-in ARIA support, screen reader
|
|
announcements, consistent browser styling, works everywhere.
|
|
</div>
|
|
|
|
<progress max="100" value="32">32%</progress>
|
|
<p>File Upload Progress</p>
|
|
|
|
<progress max="100" value="70">70%</progress>
|
|
<p>Processing Data</p>
|
|
|
|
<progress>Loading...</progress>
|
|
<p>Indeterminate Progress (no value attribute)</p>
|
|
|
|
<div class="slider-container">
|
|
<label for="progressDemo">Adjust Progress:</label>
|
|
<input id="progressDemo" max="100" min="0" oninput="document.getElementById('demoProgress').value = this.value" type="range"
|
|
value="50">
|
|
<progress id="demoProgress" max="100" value="50">50%</progress>
|
|
</div>
|
|
|
|
<div class="code-block">
|
|
<!-- The elegant solution -->
|
|
<!-- Determinate progress -->
|
|
<progress value="32" max="100">32%</progress>
|
|
<label>File Upload: 32%</label>
|
|
|
|
<!-- Indeterminate progress (no value) -->
|
|
<progress>Loading...</progress>
|
|
|
|
<!-- With form integration -->
|
|
<form>
|
|
<label for="file">Choose file:</label>
|
|
<input type="file" id="file" name="file">
|
|
<progress id="uploadProgress" style="display:none"></progress>
|
|
</form>
|
|
|
|
<script>
|
|
// Simple file upload with native progress
|
|
document.getElementById('file').addEventListener('change', function(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const progress = document.getElementById('uploadProgress');
|
|
progress.style.display = 'block';
|
|
progress.removeAttribute('value'); // Indeterminate
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.upload.addEventListener('progress', function(e) {
|
|
if (e.lengthComputable) {
|
|
// Native progress element handles everything
|
|
progress.value = e.loaded;
|
|
progress.max = e.total;
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('load', function() {
|
|
progress.style.display = 'none';
|
|
});
|
|
|
|
// Upload logic...
|
|
});
|
|
</script>
|
|
|
|
<!-- CSS customization (optional) -->
|
|
<style>
|
|
progress {
|
|
width: 100%;
|
|
height: 20px;
|
|
}
|
|
|
|
/* Webkit browsers */
|
|
progress::-webkit-progress-bar {
|
|
background-color: #f0f0f0;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
progress::-webkit-progress-value {
|
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
/* Firefox */
|
|
progress::-moz-progress-bar {
|
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
|
border-radius: 10px;
|
|
}
|
|
</style>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="pros">✅ Native Progress Advantages:</h4>
|
|
<ul>
|
|
<li>Semantic HTML element with inherent meaning</li>
|
|
<li>Built-in ARIA support for screen readers</li>
|
|
<li>Automatic progress announcements</li>
|
|
<li>Indeterminate state without extra code</li>
|
|
<li>Consistent styling across browsers</li>
|
|
<li>Form integration and validation support</li>
|
|
<li>Hardware acceleration for smooth animations</li>
|
|
<li>Automatic value/max ratio calculations</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DATE PICKERS -->
|
|
<div class="section">
|
|
<h2 class="section-title">📅 Date Pickers</h2>
|
|
<div class="comparison-grid">
|
|
<div class="column bad">
|
|
<h3>JavaScript Date Picker Hell <span class="perf-indicator perf-slow">BLOATED</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>⚠️ Accessibility Nightmare:</strong> Keyboard navigation broken, screen reader confusion, custom date
|
|
format parsing errors, focus management disasters, mobile UX inconsistencies.
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="js-date">Select Date (JavaScript)</label>
|
|
<input id="js-date" onclick="openDatePicker()" placeholder="MM/DD/YYYY" readonly type="text">
|
|
<div id="datePicker"
|
|
style="display:none; position:absolute; background:white; border:1px solid #ccc; z-index:1000;">
|
|
<!-- Imagine 200+ lines of calendar HTML here -->
|
|
<p style="padding:20px; color:#666;">
|
|
[Complex calendar widget would be here]<br>
|
|
Requires: jQuery UI, Moment.js, custom CSS,<br>
|
|
keyboard handlers, ARIA implementation,<br>
|
|
mobile touch events, and your sanity.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="code-block">
|
|
<!-- The complexity monster -->
|
|
<input type="text" id="datePicker" placeholder="Select date...">
|
|
|
|
<!-- Include massive libraries -->
|
|
<script src="jquery.min.js"></script>
|
|
<script src="moment.min.js"></script>
|
|
<script src="jquery-ui.min.js"></script>
|
|
<link rel="stylesheet" href="jquery-ui.css">
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
$('#datePicker').datepicker({
|
|
dateFormat: 'mm/dd/yy',
|
|
showOtherMonths: true,
|
|
selectOtherMonths: true,
|
|
changeMonth: true,
|
|
changeYear: true,
|
|
yearRange: '1900:2100',
|
|
showButtonPanel: true,
|
|
closeText: 'Close',
|
|
currentText: 'Today',
|
|
|
|
// Accessibility nightmare
|
|
beforeShow: function(input, inst) {
|
|
// Manual ARIA setup
|
|
inst.dpDiv.attr('role', 'dialog');
|
|
inst.dpDiv.attr('aria-label', 'Choose date');
|
|
},
|
|
|
|
// Custom keyboard navigation
|
|
onSelect: function(dateText, inst) {
|
|
// Manual validation
|
|
var selectedDate = moment(dateText, 'MM/DD/YYYY');
|
|
if (!selectedDate.isValid()) {
|
|
alert('Invalid date format');
|
|
return false;
|
|
}
|
|
|
|
// Update ARIA attributes
|
|
$(this).attr('aria-expanded', 'false');
|
|
},
|
|
|
|
// Handle ESC key
|
|
onClose: function() {
|
|
$(this).focus();
|
|
}
|
|
});
|
|
|
|
// Mobile responsive handling
|
|
if ($(window).width() < 768) {
|
|
$('#datePicker').datepicker('option', 'numberOfMonths', 1);
|
|
}
|
|
|
|
// Custom validation
|
|
$('#datePicker').on('blur', function() {
|
|
var value = $(this).val();
|
|
var date = moment(value, 'MM/DD/YYYY', true);
|
|
|
|
if (value && !date.isValid()) {
|
|
$(this).addClass('error');
|
|
$(this).attr('aria-invalid', 'true');
|
|
} else {
|
|
$(this).removeClass('error');
|
|
$(this).attr('aria-invalid', 'false');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Additional 500+ lines for:
|
|
// - Custom date formatting
|
|
// - Timezone handling
|
|
// - Localization
|
|
// - Theme customization
|
|
// - Event handling
|
|
// - Mobile touch support
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="cons">❌ Date Picker Disasters:</h4>
|
|
<ul>
|
|
<li>Massive JavaScript libraries (jQuery UI = 250KB+)</li>
|
|
<li>Complex keyboard navigation implementation</li>
|
|
<li>Screen reader compatibility requires custom ARIA</li>
|
|
<li>Mobile UX often breaks or feels non-native</li>
|
|
<li>Date format parsing errors and edge cases</li>
|
|
<li>Timezone handling becomes a nightmare</li>
|
|
<li>Localization requires additional libraries</li>
|
|
<li>Theming and styling consistency issues</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column good">
|
|
<h3>Native Date Input Paradise <span class="perf-indicator perf-fast">PERFECT</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>🎯 Accessibility Perfection:</strong> Native keyboard navigation, screen reader support,
|
|
platform-consistent UX, automatic validation, works on all devices with zero configuration.
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-date">Date</label>
|
|
<input id="native-date" max="2030-12-31" min="2020-01-01"
|
|
name="date" type="date"
|
|
value="2024-06-15">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-datetime">Date and Time</label>
|
|
<input id="native-datetime" max="2024-12-31T23:59" min="2024-01-01T00:00"
|
|
name="datetime" type="datetime-local">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-time">Time</label>
|
|
<input id="native-time" max="17:00" min="09:00"
|
|
name="time" step="900" type="time">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-month">Month</label>
|
|
<input id="native-month" name="month" type="month">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-week">Week</label>
|
|
<input id="native-week" name="week" type="week">
|
|
</div>
|
|
|
|
<div class="code-block">
|
|
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="pros">✅ Native Date Input Excellence:</h4>
|
|
<ul>
|
|
<li>Zero JavaScript required - works out of the box</li>
|
|
<li>Native OS date picker on mobile devices</li>
|
|
<li>Automatic keyboard navigation and accessibility</li>
|
|
<li>Built-in date validation and format handling</li>
|
|
<li>Consistent UX that users already understand</li>
|
|
<li>Automatic localization (user's preferred format)</li>
|
|
<li>Min/max date constraints built-in</li>
|
|
<li>Form validation integration works perfectly</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SEARCH & AUTOCOMPLETE -->
|
|
<div class="section">
|
|
<h2 class="section-title">🔍 Search & Autocomplete</h2>
|
|
<div class="comparison-grid">
|
|
<div class="column bad">
|
|
<h3>JavaScript Autocomplete Chaos <span class="perf-indicator perf-slow">COMPLEX</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>⚠️ Accessibility Nightmare:</strong> Custom dropdown management, keyboard navigation bugs, screen reader
|
|
confusion, focus trapping issues, ARIA implementation gaps.
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="js-search">City Search (JavaScript)</label>
|
|
<input autocomplete="off" id="js-search" onkeyup="filterCities(this.value)"
|
|
placeholder="Type to search cities..." type="text">
|
|
<div id="js-results"
|
|
style="display:none; position:absolute; background:var(--color-card); border:1px solid var(--color-border); max-height:200px; overflow-y:auto; z-index:100;">
|
|
<!-- Results populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="code-block">
|
|
<!-- The overcomplicated approach -->
|
|
<input type="text" id="search" placeholder="Search..."
|
|
onkeyup="handleSearch(event)"
|
|
onkeydown="handleKeyNav(event)"
|
|
onfocus="showResults()"
|
|
onblur="hideResults()">
|
|
<div id="results" class="dropdown-results"></div>
|
|
|
|
<script>
|
|
let currentFocus = -1;
|
|
const cities = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'];
|
|
|
|
function handleSearch(e) {
|
|
const value = e.target.value.toLowerCase();
|
|
const results = document.getElementById('results');
|
|
|
|
if (!value) {
|
|
results.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const filtered = cities.filter(city =>
|
|
city.toLowerCase().includes(value)
|
|
);
|
|
|
|
// Build HTML
|
|
results.innerHTML = filtered.map((city, index) =>
|
|
`<div class="result-item"
|
|
onclick="selectCity('${city}')"
|
|
data-index="${index}">${city}</div>`
|
|
).join('');
|
|
|
|
results.style.display = 'block';
|
|
currentFocus = -1;
|
|
|
|
// Manual ARIA updates
|
|
results.setAttribute('role', 'listbox');
|
|
e.target.setAttribute('aria-expanded', 'true');
|
|
e.target.setAttribute('aria-autocomplete', 'list');
|
|
}
|
|
|
|
function handleKeyNav(e) {
|
|
const results = document.querySelectorAll('.result-item');
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
currentFocus++;
|
|
if (currentFocus >= results.length) currentFocus = 0;
|
|
setActive(results);
|
|
e.preventDefault();
|
|
} else if (e.key === 'ArrowUp') {
|
|
currentFocus--;
|
|
if (currentFocus < 0) currentFocus = results.length - 1;
|
|
setActive(results);
|
|
e.preventDefault();
|
|
} else if (e.key === 'Enter') {
|
|
if (currentFocus > -1 && results[currentFocus]) {
|
|
selectCity(results[currentFocus].textContent);
|
|
}
|
|
e.preventDefault();
|
|
} else if (e.key === 'Escape') {
|
|
hideResults();
|
|
}
|
|
}
|
|
|
|
function setActive(results) {
|
|
results.forEach((item, index) => {
|
|
item.classList.toggle('active', index === currentFocus);
|
|
item.setAttribute('aria-selected', index === currentFocus);
|
|
});
|
|
}
|
|
|
|
function selectCity(city) {
|
|
document.getElementById('search').value = city;
|
|
hideResults();
|
|
}
|
|
|
|
function hideResults() {
|
|
document.getElementById('results').style.display = 'none';
|
|
document.getElementById('search').setAttribute('aria-expanded', 'false');
|
|
}
|
|
|
|
// Handle click outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('#search') && !e.target.closest('#results')) {
|
|
hideResults();
|
|
}
|
|
});
|
|
|
|
// Additional complexity for:
|
|
// - Debouncing API calls
|
|
// - Loading states
|
|
// - Error handling
|
|
// - Mobile touch events
|
|
// - Performance optimization
|
|
// - Screen reader announcements
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="cons">❌ JavaScript Autocomplete Problems:</h4>
|
|
<ul>
|
|
<li>Complex keyboard navigation implementation</li>
|
|
<li>Manual ARIA attribute management</li>
|
|
<li>Focus management and blur event handling</li>
|
|
<li>Dropdown positioning and z-index issues</li>
|
|
<li>Screen reader compatibility requires extensive work</li>
|
|
<li>Mobile touch and scroll behavior inconsistencies</li>
|
|
<li>Performance issues with large datasets</li>
|
|
<li>Cross-browser event handling differences</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column good">
|
|
<h3>Native Datalist Perfection <span class="perf-indicator perf-fast">SIMPLE</span></h3>
|
|
|
|
<div class="accessibility-note">
|
|
<strong>🎯 Accessibility Paradise:</strong> Built-in keyboard navigation, screen reader support, native OS
|
|
integration, consistent UX, zero JavaScript required.
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-city">City Search (Native)</label>
|
|
<input id="native-city" list="cities" placeholder="Type or select a city..."
|
|
type="text">
|
|
<datalist id="cities">
|
|
<option value="New York">
|
|
<option value="Los Angeles">
|
|
<option value="Chicago">
|
|
<option value="Houston">
|
|
<option value="Phoenix">
|
|
<option value="Philadelphia">
|
|
<option value="San Antonio">
|
|
<option value="San Diego">
|
|
<option value="Dallas">
|
|
<option value="San Jose">
|
|
</datalist>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-browser">Browser Choice</label>
|
|
<input id="native-browser" list="browsers" placeholder="Choose your browser..."
|
|
type="text">
|
|
<datalist id="browsers">
|
|
<option label="Google Chrome" value="Chrome">
|
|
<option label="Mozilla Firefox" value="Firefox">
|
|
<option label="Apple Safari" value="Safari">
|
|
<option label="Microsoft Edge" value="Edge">
|
|
<option label="Opera Browser" value="Opera">
|
|
</datalist>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="native-email">Email Domain</label>
|
|
<input id="native-email" list="email-domains" placeholder="user@domain.com"
|
|
type="email">
|
|
<datalist id="email-domains">
|
|
<option value="@gmail.com">
|
|
<option value="@yahoo.com">
|
|
<option value="@outlook.com">
|
|
<option value="@hotmail.com">
|
|
<option value="@icloud.com">
|
|
</datalist>
|
|
</div>
|
|
|
|
<div class="code-block">
|
|
<!-- The elegant native solution -->
|
|
<label for="city-search">Choose a city:</label>
|
|
<input type="text" id="city-search" list="cities"
|
|
placeholder="Type or select...">
|
|
|
|
<datalist id="cities">
|
|
<option value="New York">
|
|
<option value="Los Angeles">
|
|
<option value="Chicago">
|
|
<option value="Houston">
|
|
<option value="Phoenix">
|
|
</datalist>
|
|
|
|
<!-- With labels for additional info -->
|
|
<input type="text" list="products">
|
|
<datalist id="products">
|
|
<option value="iPhone 15" label="Apple - Latest model">
|
|
<option value="Samsung Galaxy S24" label="Samsung - Android">
|
|
<option value="Google Pixel 8" label="Google - Pure Android">
|
|
</datalist>
|
|
|
|
<!-- Works with other input types -->
|
|
<input type="email" list="email-providers">
|
|
<datalist id="email-providers">
|
|
<option value="@gmail.com">
|
|
<option value="@yahoo.com">
|
|
<option value="@outlook.com">
|
|
</datalist>
|
|
|
|
<input type="url" list="popular-sites">
|
|
<datalist id="popular-sites">
|
|
<option value="https://github.com">
|
|
<option value="https://stackoverflow.com">
|
|
<option value="https://developer.mozilla.org">
|
|
</datalist>
|
|
|
|
<!-- Optional: Dynamic options with JavaScript -->
|
|
<script>
|
|
// Only if you need dynamic data
|
|
fetch('/api/cities')
|
|
.then(response => response.json())
|
|
.then(cities => {
|
|
const datalist = document.getElementById('cities');
|
|
cities.forEach(city => {
|
|
const option = document.createElement('option');
|
|
option.value = city.name;
|
|
option.label = city.country;
|
|
datalist.appendChild(option);
|
|
});
|
|
});
|
|
|
|
// Form validation works automatically
|
|
document.querySelector('form').addEventListener('submit', function(e) {
|
|
const input = document.getElementById('city-search');
|
|
// Browser handles validation
|
|
if (!input.validity.valid) {
|
|
input.reportValidity();
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
</script>
|
|
</div>
|
|
|
|
<div class="pros-cons">
|
|
<h4 class="pros">✅ Native Datalist Advantages:</h4>
|
|
<ul>
|
|
<li>Zero JavaScript required for basic functionality</li>
|
|
<li>Built-in keyboard navigation (arrows, enter, esc)</li>
|
|
<li>Screen reader compatible with proper announcements</li>
|
|
<li>Native OS integration and consistent UX</li>
|
|
<li>Works with form validation automatically</li>
|
|
<li>Supports additional context with label attribute</li>
|
|
<li>Integrates with different input types (email, url, etc.)</li>
|
|
<li>Graceful degradation - still works as regular input</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="summary-box">
|
|
<h3>The Path to HTML Enlightenment</h3>
|
|
<p>You've witnessed the truth: <strong>90% of modern JavaScript UI frameworks are solving problems that HTML already
|
|
solved.</strong> Native elements provide accessibility, keyboard navigation, screen reader support, consistent UX,
|
|
and semantic meaning - all for free.</p>
|
|
|
|
<p><strong>The choice is yours:</strong></p>
|
|
<ul style="text-align: left; max-width: 600px; margin: 2rem auto;">
|
|
<li>Continue fighting the platform with complex JavaScript implementations</li>
|
|
<li>Or embrace the zen of semantic HTML and progressive enhancement</li>
|
|
</ul>
|
|
|
|
<p>Stop reinventing the wheel. <strong>The web platform is your friend.</strong></p>
|
|
|
|
<div class="zen-quote">
|
|
"The best code is no code. The second best code is semantic HTML that leverages decades of browser engineering and
|
|
accessibility research."
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Minimal JavaScript for the demo comparisons only
|
|
function toggleContent(id) {
|
|
const content = document.getElementById(id);
|
|
if (content.style.display === 'none' || !content.style.display) {
|
|
content.style.display = 'block';
|
|
content.classList.add('active');
|
|
} else {
|
|
content.style.display = 'none';
|
|
content.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
function openJSModal() {
|
|
document.getElementById('jsModal').classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeJSModal() {
|
|
document.getElementById('jsModal').classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
function validateJSForm(event) {
|
|
event.preventDefault();
|
|
|
|
const email = document.getElementById('js-email').value;
|
|
const password = document.getElementById('js-password').value;
|
|
const emailError = document.getElementById('email-error');
|
|
const passwordError = document.getElementById('password-error');
|
|
|
|
let isValid = true;
|
|
|
|
// Reset errors
|
|
emailError.classList.remove('active');
|
|
passwordError.classList.remove('active');
|
|
|
|
// Email validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email)) {
|
|
emailError.classList.add('active');
|
|
isValid = false;
|
|
}
|
|
|
|
// Password validation
|
|
if (password.length < 8) {
|
|
passwordError.classList.add('active');
|
|
isValid = false;
|
|
}
|
|
|
|
if (isValid) {
|
|
alert('Form submitted! (This is just a demo)');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function startJSProgress() {
|
|
const progressBar = document.getElementById('jsProgress');
|
|
const progressText = document.getElementById('jsProgressText');
|
|
let progress = 0;
|
|
|
|
const interval = setInterval(() => {
|
|
progress += Math.random() * 15;
|
|
if (progress >= 100) {
|
|
progress = 100;
|
|
clearInterval(interval);
|
|
}
|
|
|
|
progressBar.style.width = progress + '%';
|
|
progressText.textContent = Math.round(progress) + '%';
|
|
|
|
// Change color based on progress
|
|
if (progress < 30) {
|
|
progressBar.style.background = 'linear-gradient(90deg, #ff4757, #ff6b7a)';
|
|
} else if (progress < 70) {
|
|
progressBar.style.background = 'linear-gradient(90deg, #ffa502, #ffb84d)';
|
|
} else {
|
|
progressBar.style.background = 'linear-gradient(90deg, #2ed573, #7bed9f)';
|
|
}
|
|
}, 200);
|
|
}
|
|
|
|
function openDatePicker() {
|
|
const picker = document.getElementById('datePicker');
|
|
picker.style.display = picker.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
|
|
function toggleAccordion(id) {
|
|
const content = document.getElementById(id);
|
|
if (content.style.display === 'none' || !content.style.display) {
|
|
content.style.display = 'block';
|
|
} else {
|
|
content.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
const cities = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', 'Philadelphia', 'San Antonio'];
|
|
|
|
function filterCities(value) {
|
|
const results = document.getElementById('js-results');
|
|
|
|
if (!value) {
|
|
results.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const filtered = cities.filter(city =>
|
|
city.toLowerCase().includes(value.toLowerCase())
|
|
);
|
|
|
|
if (filtered.length === 0) {
|
|
results.innerHTML = '<div style="padding: 10px; color: #999;">No cities found</div>';
|
|
results.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
results.innerHTML = filtered.map(city =>
|
|
`<div style="padding: 10px; cursor: pointer; border-bottom: 1px solid var(--color-border);"
|
|
onmouseover="this.style.background='var(--color-primary-hover)'"
|
|
onmouseout="this.style.background=''"
|
|
onclick="selectCity('${city}')">${city}</div>`
|
|
).join('');
|
|
|
|
results.style.display = 'block';
|
|
}
|
|
|
|
function selectCity(city) {
|
|
document.getElementById('js-search').value = city;
|
|
document.getElementById('js-results').style.display = 'none';
|
|
}
|
|
|
|
// Hide results when clicking outside
|
|
document.addEventListener('click', function (e) {
|
|
if (!e.target.closest('#js-search') && !e.target.closest('#js-results')) {
|
|
document.getElementById('js-results').style.display = 'none';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|