HTML
Teknik HTML modern untuk aplikasi web yang kompleks dan accessible
Pengenalan
HTML modern bukan hanya tentang markup sederhana. courses ini akan mengajarkan Anda semantic HTML, accessibility, form validation, dan teknik-teknik advanced untuk membangun aplikasi web yang robust.
Semantic HTML
Mengapa Semantic HTML Penting?
Semantic HTML memberikan makna pada struktur dokumen, membantu:
- Search engines memahami konten
- Screen readers memberikan pengalaman yang lebih baik
- Developer memahami struktur kode
- Maintenance yang lebih mudah
Elemen Semantic Utama
<!-- Header dengan navigasi -->
<header>
<nav>
<ul>
<li><a href="/">Beranda</a></li>
<li><a href="/tentang">Tentang</a></li>
<li><a href="/kontak">Kontak</a></li>
</ul>
</nav>
</header>
<!-- Konten utama -->
<main>
<!-- Artikel dengan struktur lengkap -->
<article>
<header>
<h1>Judul Artikel</h1>
<p>
<time datetime="2024-01-15">15 Januari 2024</time>
oleh <address>John Doe</address>
</p>
</header>
<section>
<h2>Pendahuluan</h2>
<p>Konten pendahuluan...</p>
</section>
<section>
<h2>Pembahasan</h2>
<p>Konten pembahasan...</p>
</section>
<footer>
<p>Tags: <a href="/tag/html">HTML</a>, <a href="/tag/web">Web</a></p>
</footer>
</article>
<!-- Sidebar dengan konten terkait -->
<aside>
<h2>Artikel Terkait</h2>
<ul>
<li><a href="/artikel-1">Artikel 1</a></li>
<li><a href="/artikel-2">Artikel 2</a></li>
</ul>
</aside>
</main>
<!-- Footer -->
<footer>
<p>© 2024 Perusahaan Anda. Hak cipta dilindungi.</p>
</footer>Figure dan Figcaption
<figure>
<img src="/grafik-penjualan.png" alt="Grafik penjualan Q1 2024">
<figcaption>
Grafik menunjukkan peningkatan penjualan sebesar 45% di Q1 2024
</figcaption>
</figure>
<figure>
<pre><code>
function hello() {
console.log('Hello World');
}
</code></pre>
<figcaption>Contoh fungsi JavaScript sederhana</figcaption>
</figure>Details dan Summary
<details>
<summary>Apa itu HTML Semantic?</summary>
<p>
HTML Semantic adalah penggunaan elemen HTML yang memberikan makna
pada struktur konten, bukan hanya presentasi visual.
</p>
</details>
<details open>
<summary>FAQ: Bagaimana cara memulai?</summary>
<p>Mulai dengan mempelajari elemen-elemen semantic dasar seperti
header, nav, main, article, section, aside, dan footer.</p>
</details>Form Advanced
Form Validation HTML5
<form action="/submit" method="POST" novalidate>
<!-- Email dengan validasi -->
<div>
<label for="email">Email:</label>
<input
type="email"
id="email"
name="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
placeholder="nama@example.com"
>
<span class="error" aria-live="polite"></span>
</div>
<!-- Password dengan requirements -->
<div>
<label for="password">Password:</label>
<input
type="password"
id="password"
name="password"
required
minlength="8"
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
title="Minimal 8 karakter, harus mengandung huruf besar, huruf kecil, dan angka"
>
<span class="error" aria-live="polite"></span>
</div>
<!-- Number dengan range -->
<div>
<label for="age">Umur:</label>
<input
type="number"
id="age"
name="age"
min="18"
max="100"
step="1"
required
>
</div>
<!-- Date dengan range -->
<div>
<label for="birthdate">Tanggal Lahir:</label>
<input
type="date"
id="birthdate"
name="birthdate"
min="1920-01-01"
max="2006-12-31"
required
>
</div>
<!-- URL validation -->
<div>
<label for="website">Website:</label>
<input
type="url"
id="website"
name="website"
placeholder="https://example.com"
>
</div>
<!-- Tel dengan pattern -->
<div>
<label for="phone">Telepon:</label>
<input
type="tel"
id="phone"
name="phone"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
placeholder="123-456-7890"
>
</div>
<button type="submit">Kirim</button>
</form>Custom Validation dengan JavaScript
<form id="registrationForm">
<div>
<label for="username">Username:</label>
<input
type="text"
id="username"
name="username"
required
minlength="3"
>
<span class="error"></span>
</div>
<div>
<label for="confirmPassword">Konfirmasi Password:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
required
>
<span class="error"></span>
</div>
<button type="submit">Daftar</button>
</form>
<script>
const form = document.getElementById('registrationForm');
const username = document.getElementById('username');
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirmPassword');
// Custom validation
username.addEventListener('input', function() {
if (username.validity.valueMissing) {
username.setCustomValidity('Username harus diisi');
} else if (username.validity.tooShort) {
username.setCustomValidity('Username minimal 3 karakter');
} else if (!/^[a-zA-Z0-9_]+$/.test(username.value)) {
username.setCustomValidity('Username hanya boleh huruf, angka, dan underscore');
} else {
username.setCustomValidity('');
}
username.reportValidity();
});
// Password match validation
confirmPassword.addEventListener('input', function() {
if (confirmPassword.value !== password.value) {
confirmPassword.setCustomValidity('Password tidak cocok');
} else {
confirmPassword.setCustomValidity('');
}
confirmPassword.reportValidity();
});
form.addEventListener('submit', function(e) {
if (!form.checkValidity()) {
e.preventDefault();
// Show all errors
const inputs = form.querySelectorAll('input');
inputs.forEach(input => {
const error = input.nextElementSibling;
if (!input.validity.valid) {
error.textContent = input.validationMessage;
} else {
error.textContent = '';
}
});
}
});
</script>Input Types Modern
<!-- Color picker -->
<label for="color">Pilih Warna:</label>
<input type="color" id="color" name="color" value="#ff0000">
<!-- Range slider -->
<label for="volume">Volume:</label>
<input type="range" id="volume" name="volume" min="0" max="100" value="50">
<output for="volume">50</output>
<!-- File upload dengan accept -->
<label for="avatar">Upload Avatar:</label>
<input
type="file"
id="avatar"
name="avatar"
accept="image/png, image/jpeg"
multiple
>
<!-- Search dengan datalist -->
<label for="search">Cari Produk:</label>
<input
type="search"
id="search"
name="search"
list="products"
autocomplete="off"
>
<datalist id="products">
<option value="Laptop">
<option value="Mouse">
<option value="Keyboard">
<option value="Monitor">
</datalist>Accessibility (A11y)
ARIA Attributes
<!-- Navigation dengan ARIA -->
<nav aria-label="Navigasi Utama">
<ul role="menubar">
<li role="none">
<a href="/" role="menuitem">Beranda</a>
</li>
<li role="none">
<a href="/produk" role="menuitem" aria-haspopup="true" aria-expanded="false">
Produk
</a>
<ul role="menu" aria-label="Submenu Produk" hidden>
<li role="none">
<a href="/produk/laptop" role="menuitem">Laptop</a>
</li>
<li role="none">
<a href="/produk/phone" role="menuitem">Phone</a>
</li>
</ul>
</li>
</ul>
</nav>
<!-- Alert messages -->
<div role="alert" aria-live="assertive" aria-atomic="true">
<p>Formulir berhasil dikirim!</p>
</div>
<!-- Loading state -->
<button aria-busy="true" aria-label="Memuat data...">
<span aria-hidden="true">⏳</span>
Memuat...
</button>
<!-- Tab interface -->
<div role="tablist" aria-label="Pengaturan Akun">
<button
role="tab"
aria-selected="true"
aria-controls="profile-panel"
id="profile-tab"
>
Profil
</button>
<button
role="tab"
aria-selected="false"
aria-controls="security-panel"
id="security-tab"
>
Keamanan
</button>
</div>
<div role="tabpanel" id="profile-panel" aria-labelledby="profile-tab">
<h2>Pengaturan Profil</h2>
<!-- Konten profil -->
</div>
<div role="tabpanel" id="security-panel" aria-labelledby="security-tab" hidden>
<h2>Pengaturan Keamanan</h2>
<!-- Konten keamanan -->
</div>Skip Links
<body>
<a href="#main-content" class="skip-link">
Lewati ke konten utama
</a>
<header>
<!-- Header content -->
</header>
<main id="main-content" tabindex="-1">
<!-- Main content -->
</main>
</body>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>Focus Management
<!-- Visible focus indicator -->
<style>
:focus {
outline: 3px solid #4A90E2;
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 3px solid #4A90E2;
outline-offset: 2px;
}
</style>
<!-- Modal dengan focus trap -->
<div role="dialog" aria-labelledby="modal-title" aria-modal="true">
<h2 id="modal-title">Konfirmasi Hapus</h2>
<p>Apakah Anda yakin ingin menghapus item ini?</p>
<button>Batal</button>
<button>Hapus</button>
</div>
<script>
// Focus trap implementation
const modal = document.querySelector('[role="dialog"]');
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
e.preventDefault();
}
}
}
if (e.key === 'Escape') {
closeModal();
}
});
</script>Meta Tags dan SEO
Essential Meta Tags
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Primary Meta Tags -->
<title>Judul Halaman - Nama Website</title>
<meta name="title" content="Judul Halaman - Nama Website">
<meta name="description" content="Deskripsi halaman yang menarik dan informatif, maksimal 160 karakter">
<meta name="keywords" content="keyword1, keyword2, keyword3">
<meta name="author" content="Nama Penulis">
<meta name="robots" content="index, follow">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/">
<meta property="og:title" content="Judul Halaman - Nama Website">
<meta property="og:description" content="Deskripsi halaman yang menarik">
<meta property="og:image" content="https://example.com/image.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://example.com/">
<meta property="twitter:title" content="Judul Halaman - Nama Website">
<meta property="twitter:description" content="Deskripsi halaman yang menarik">
<meta property="twitter:image" content="https://example.com/image.jpg">
<!-- Favicon -->
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<!-- Canonical URL -->
<link rel="canonical" href="https://example.com/halaman-ini">
<!-- Alternate languages -->
<link rel="alternate" hreflang="id" href="https://example.com/id/halaman">
<link rel="alternate" hreflang="en" href="https://example.com/en/page">
</head>
<body>
<!-- Content -->
</body>
</html>Structured Data (JSON-LD)
<!-- Article Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Judul Artikel",
"image": "https://example.com/image.jpg",
"author": {
"@type": "Person",
"name": "John Doe"
},
"publisher": {
"@type": "Organization",
"name": "Nama Organisasi",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
},
"datePublished": "2024-01-15",
"dateModified": "2024-01-20"
}
</script>
<!-- Product Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Nama Produk",
"image": "https://example.com/product.jpg",
"description": "Deskripsi produk",
"brand": {
"@type": "Brand",
"name": "Nama Brand"
},
"offers": {
"@type": "Offer",
"url": "https://example.com/product",
"priceCurrency": "IDR",
"price": "1000000",
"availability": "https://schema.org/InStock"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "89"
}
}
</script>Web Components
Custom Elements
<user-card
name="John Doe"
email="john@example.com"
avatar="/avatar.jpg"
></user-card>
<script>
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const name = this.getAttribute('name');
const email = this.getAttribute('email');
const avatar = this.getAttribute('avatar');
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
max-width: 300px;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
}
.name {
font-size: 18px;
font-weight: bold;
margin: 8px 0 4px;
}
.email {
color: #666;
font-size: 14px;
}
</style>
<div class="card">
<img src="${avatar}" alt="${name}" class="avatar">
<div class="name">${name}</div>
<div class="email">${email}</div>
<slot></slot>
</div>
`;
}
static get observedAttributes() {
return ['name', 'email', 'avatar'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.connectedCallback();
}
}
}
customElements.define('user-card', UserCard);
</script>Template dan Slot
<template id="product-card-template">
<style>
.product-card {
border: 1px solid #ddd;
padding: 16px;
border-radius: 8px;
}
.product-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
}
.product-title {
font-size: 18px;
font-weight: bold;
margin: 12px 0 8px;
}
.product-price {
color: #e74c3c;
font-size: 20px;
font-weight: bold;
}
</style>
<div class="product-card">
<img class="product-image" src="" alt="">
<h3 class="product-title"></h3>
<div class="product-price"></div>
<slot name="actions"></slot>
</div>
</template>
<product-card
image="/product.jpg"
title="Nama Produk"
price="Rp 1.000.000"
>
<div slot="actions">
<button>Beli Sekarang</button>
<button>Tambah ke Keranjang</button>
</div>
</product-card>
<script>
class ProductCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const template = document.getElementById('product-card-template');
const content = template.content.cloneNode(true);
this.shadowRoot.appendChild(content);
}
connectedCallback() {
const image = this.getAttribute('image');
const title = this.getAttribute('title');
const price = this.getAttribute('price');
this.shadowRoot.querySelector('.product-image').src = image;
this.shadowRoot.querySelector('.product-image').alt = title;
this.shadowRoot.querySelector('.product-title').textContent = title;
this.shadowRoot.querySelector('.product-price').textContent = price;
}
}
customElements.define('product-card', ProductCard);
</script>Performance Optimization
Lazy Loading
<!-- Images -->
<img
src="placeholder.jpg"
data-src="actual-image.jpg"
alt="Description"
loading="lazy"
width="800"
height="600"
>
<!-- Iframes -->
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
loading="lazy"
width="560"
height="315"
title="Video Title"
></iframe>
<!-- Native lazy loading dengan IntersectionObserver fallback -->
<script>
if ('loading' in HTMLImageElement.prototype) {
// Browser supports native lazy loading
const images = document.querySelectorAll('img[loading="lazy"]');
images.forEach(img => {
img.src = img.dataset.src;
});
} else {
// Fallback to IntersectionObserver
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
const images = document.querySelectorAll('img.lazy');
images.forEach(img => imageObserver.observe(img));
}
</script>Resource Hints
<head>
<!-- DNS Prefetch -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<!-- Preconnect -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Prefetch -->
<link rel="prefetch" href="/next-page.html">
<!-- Preload -->
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>
<!-- Prerender (use carefully) -->
<link rel="prerender" href="/likely-next-page.html">
</head>Latihan Praktis
Project 1: Form Registrasi Lengkap
Buat form registrasi dengan:
- Validasi email, password, dan konfirmasi password
- Password strength indicator
- Terms and conditions checkbox
- Accessible error messages
- Loading state saat submit
Project 2: Accessible Navigation
Buat navigation menu dengan:
- Keyboard navigation (Tab, Arrow keys, Enter, Escape)
- ARIA attributes yang tepat
- Mobile responsive dengan hamburger menu
- Focus management
- Skip links
Project 3: Product Card Component
Buat Web Component untuk product card dengan:
- Shadow DOM
- Slots untuk custom content
- Observed attributes
- Event handling
- Styling yang encapsulated
Kesimpulan
HTML modern adalah fondasi yang kuat untuk aplikasi web. Dengan memahami semantic HTML, accessibility, form validation, dan Web Components, Anda dapat membangun aplikasi yang:
- Mudah di-maintain
- Accessible untuk semua pengguna
- SEO-friendly
- Performant
- Scalable