সরাসরি পর্বে যান
পর্ব ৫২ · ৫৯-এর মধ্যে · মডিউল ৭

HTML-CSS আর JavaScript-এর মিলন — DOM, template আর Web Components

Where HTML, CSS & JavaScript Meet — the DOM, State-Driven CSS, template, Custom Elements & Shadow DOM

Taught in Bangla, this lesson shows how the DOM connects HTML, CSS and JavaScript: selecting elements with querySelector, toggling classes, driving styles from data-* and aria-* state, reading and writing CSS variables, basic click and input events, the template element, custom elements and lifecycle callbacks, Shadow DOM with :host, ::part, slots and ::slotted, declarative shadow DOM, accessibility and when to use Web Components or a framework.

এই পর্বে যা শিখবেন

  • DOM কীভাবে HTML, CSS আর JavaScript-কে এক জায়গায় জোড়ে, আর JavaScript-এর একটি বদলের পর ব্রাউজার কী কী করে
  • querySelector, classList, data-* আর aria-* দিয়ে “অবস্থা” বদলানো এবং সেই অবস্থা দেখে CSS-এ চেহারা ঠিক করা
  • JavaScript থেকে CSS variable পড়া-লেখা আর click ও input event-এর মৌলিক ব্যবহার
  • template, custom element, lifecycle callback, Shadow DOM, :host, ::part, slot, ::slotted আর declarative shadow DOM
  • custom element-এর অ্যাক্সেসিবিলিটি, “HTML web components” ধরনের progressive enhancement, আর কখন Web Components, কখন framework

১ · তিন ভাষার সেতু — DOM আসলে কী

একটি নতুন বাড়ির কথা ভাবুন। ইট-রড-ঢালাই হলো কাঠামো — সেটি HTML. দেয়ালের রং, টাইলস, পর্দা — সেটি CSS. আর বিদ্যুতের সুইচ, যা টিপলে বাতি জ্বলে, ফ্যান ঘোরে — সেটি JavaScript. এই কোর্সের একান্নটি পর্ব ধরে আমরা কাঠামো আর রং নিয়েই কাজ করেছি। এবার দেখব সুইচের তার কোথায় গিয়ে লাগে।

উত্তর হলো DOM (Document Object Model) — ব্রাউজারের বানানো একটি মডেল। পর্ব ০১-এ দেখেছিলেন, ব্রাউজার HTML ফাইল পড়ে মেমরিতে একটি গাছ বানায়; পর্ব ০৩-এ দেখেছিলেন, সেই গাছের প্রতিটি element একটি node, যার parent, child আর sibling আছে। মনে রাখার মতো মূল কথা হলো: ব্রাউজার আপনার HTML ফাইল দেখায় না, দেখায় DOM. ফাইলটি একবার পড়া হয়ে গেলে কাজ শেষ; এরপর পর্দায় যা আছে, তা DOM-এর ছবি।

CSS-ও আসলে DOM-কেই সাজায়, ফাইলকে নয়। আর JavaScript DOM-এ হাত দিতে পারে — নতুন element যোগ, লেখা বদল, class বসানো। প্রতিটি বদলের পর ব্রাউজার নিজে থেকেই হিসাব করে কোন CSS নিয়ম এখন খাটে, তারপর দরকার হলে লেআউট আর রং নতুন করে আঁকে। নিচে “পরে” চেপে পুরো পথটি দেখুন:

  1. HTML ফাইলindex.htmlশুধু লেখা
  2. DOMdocumentজীবন্ত গাছ
  3. JavaScriptclassListগাছ বদলায়
  4. পর্দাstyle → layout → paintনতুন ছবি
index.htmlসার্ভার থেকে আসা লেখা
<div class="card">
  <h3>৩০ জিবি প্যাকেজ</h3>
  <button>বাছাই করুন</button>
</div>
DOMparser-এর বানানো গাছ
div class="card"
├── h3 → "৩০ জিবি প্যাকেজ"
└── button → "বাছাই করুন"
JavaScriptগাছের একটি node-এ বদল
card.classList.add('is-selected');

div class="card is-selected"
├── h3
└── button
ব্রাউজারCSS আবার মেলানো
.card.is-selected { background: #ccfbf1; }
↑ এই নিয়ম এখন খাটে → নতুন রং আঁকা হলো
  1. শুধু লেখা। সার্ভার থেকে আসা HTML একটি সাধারণ টেক্সট ফাইল — ঠিক যেন বাড়ির নকশার কাগজ। এর নিজের কোনো “অবস্থা” নেই।
  2. Parser গাছ বানাল। ব্রাউজারের HTML parser ফাইল পড়ে DOM বানায়, পাশাপাশি CSS পড়ে CSSOM (পর্ব ০১)। এখন থেকে পর্দা DOM-কেই অনুসরণ করবে, ফাইলকে নয়।
  3. JavaScript গাছ বদলাল। বোতাম চাপায় স্ক্রিপ্ট div-এ একটি নতুন class যোগ করল। ফাইল বদলায়নি — বদলেছে মেমরির গাছ। DevTools-এর Elements প্যানেলে আপনি এই জীবন্ত গাছটিই দেখেন, তাই সেখানে নতুন class ঝলক দিয়ে ওঠে।
  4. ব্রাউজার আবার মেলাল। ব্রাউজার style recalculation চালিয়ে দেখল .card.is-selected এখন মেলে, তারপর রং বদলে আবার আঁকল। লক্ষ করুন: JavaScript রং ছোঁয়নি — শুধু একটি নাম বসিয়েছে। পুরো পর্বের মূলনীতি এখানেই লুকিয়ে আছে।

এই পর্বটি একজন HTML-CSS ডেভেলপারের চোখে JavaScript — যতটুকু না জানলে আধুনিক ওয়েবে HTML-CSS-এর কাজ অসম্পূর্ণ থাকে, ঠিক ততটুকু। ভ্যারিয়েবল, ফাংশন, লুপ, event-এর গভীর নিয়ম শিখতে চাইলে আমাদের JavaScript কোর্সের DOM-এর পর্ব আর event-এর পর্ব দেখুন।

২ · element খোঁজা আর class বদলানো — querySelector ও classList

DOM-এ কিছু বদলাতে হলে আগে element-টিকে হাতে পেতে হয়। মজার খবর হলো, এর জন্য নতুন কোনো ভাষা শিখতে হবে না — আপনার জানা CSS selector-ই চাবি। document.querySelector('#pkg') পাতার প্রথম যে element-টি #pkg selector-এ মেলে, সেটি ফেরত দেয় (না পেলে null); querySelectorAll('.card') দেয় মেলে যাওয়া সবগুলো। পর্ব ১৭ আর ৩৪-এ শেখা প্রতিটি selector — attribute, combinator, :has() — এখানে কাজ করে, কারণ ব্রাউজার CSS-এর একই selector ইঞ্জিন দিয়ে এগুলো চালায়।

হাতে পেলে সবচেয়ে কাজের হাতিয়ার classList: add(), remove(), contains() আর toggle(). toggle('x') class থাকলে সরায়, না থাকলে বসায়, আর ফেরত দেয় শেষ অবস্থা — true বা false. ছোট একটি উদাহরণে দেখুন — একটি মোবাইল ইন্টারনেট প্যাকেজ বাছাইয়ের কার্ড:

<style>
  .card { max-width: 280px; padding: 12px 16px; border: 2px solid #94a3b8; border-radius: 12px; }
  .card.is-selected { border-color: #0f766e; background: #ccfbf1; }
</style>
<div class="card" id="pkg">
  <h3>ইন্টারনেট প্যাকেজ — ৩০ জিবি</h3>
  <p>মেয়াদ ৩০ দিন · ৳৫০০ (উদাহরণ)</p>
  <button type="button" id="pick">এটি বাছাই করুন</button>
</div>
<script>
  const card = document.querySelector('#pkg');
  const btn = document.querySelector('#pick');
  btn.addEventListener('click', () => {
    const on = card.classList.toggle('is-selected');
    btn.textContent = on ? 'বাছাই বাতিল করুন' : 'এটি বাছাই করুন';
    console.log('class এখন:', card.className);
  });
</script>

বোতাম চাপলে কার্ডের বর্ডার সবুজ আর পটভূমি হালকা সবুজ হয়, বোতামের লেখা বদলে যায়, আর নিচে আসে class এখন: card is-selected. আবার চাপলে সব আগের মতো। খেয়াল করুন, স্ক্রিপ্টে কোনো রং নেই — রং আছে শুধু CSS-এ। চেষ্টা করুন: .card.is-selected নিয়মে scale: 1.03; আর .card-এ transition: scale .2s; যোগ করুন — JavaScript একটুও না বদলে কার্ডটি এখন মসৃণভাবে বড় হবে (পর্ব ৩৮-এর transition দেখুন)। তারপর '#pkg'-কে '.nothing' করে দেখুন — null-এর ওপর classList পড়তে গিয়ে কনসোলে error আসবে; এটিই নতুনদের সবচেয়ে চেনা ভুল।

যে কয়টি DOM-কাজ HTML-CSS ডেভেলপারের রোজ লাগে

কাজলেখার ধরনমনে রাখুন
একটি element খোঁজাdocument.querySelector('nav a')প্রথমটি, নয়তো null
সবগুলো খোঁজাdocument.querySelectorAll('.card')স্থির তালিকা, পরে যোগ হওয়া element ধরে না
class বদলel.classList.toggle('is-open')চেহারার নিয়ম থাকবে CSS-এ
attribute পড়া-লেখাel.getAttribute('aria-expanded'), setAttribute()মান সবসময় লেখা (string)
data-* পড়া-লেখাel.dataset.state = 'open'HTML-এ হয় data-state="open"
লেখা বসানোel.textContent = 'নতুন লেখা'নিরাপদ — HTML হিসেবে পড়া হয় না
দেখানো-লুকানোel.hidden = trueHTML-এর নিজস্ব hidden attribute (পর্ব ২৩)

innerHTML নিয়ে সাবধান। el.innerHTML = '…' লেখাটিকে HTML হিসেবে পার্স করে বসায়। লেখাটি যদি ব্যবহারকারীর কাছ থেকে আসে — একটি মন্তব্য, একটি নাম — তবে কেউ সেখানে <img src=x onerror=…> লিখে আপনার পাতায় নিজের স্ক্রিপ্ট চালাতে পারে; একে বলে XSS. নিয়ম সহজ: লেখা বসাতে textContent, কাঠামো বানাতে <template> (অংশ ৫) বা createElement().

৩ · “CSS চেহারার, JS অবস্থার” — data-* আর aria-* দিয়ে CSS চালানো

২০০৬ সালে jQuery আসার পর এক দশক ধরে ওয়েবে একটি অভ্যাস খুব চলেছে: JavaScript দিয়ে সরাসরি চেহারা বসানো — el.style.display = 'block', el.style.background = 'red'. দেখতে সহজ, কিন্তু এতে তিনটি বড় সমস্যা হয়:

  • CSS হেরে যায়। el.style আসলে একটি inline style, যার specificity যেকোনো selector-এর চেয়ে বেশি (পর্ব ১৮)। পরে CSS ফাইলে dark mode বা মোবাইলের জন্য রং বদলাতে চাইলে !important ছাড়া উপায় থাকে না।
  • নকশা দুই জায়গায় ছড়িয়ে পড়ে। ডিজাইনার রং বদলাতে বললে CSS আর JavaScript — দুই ফাইলেই খুঁজতে হয়। Separation of concerns-এর নীতি ভেঙে যায়।
  • Screen reader কিছুই জানে না। একটি মেনু পর্দায় খুলল, কিন্তু যিনি চোখে দেখেন না, তাঁর কাছে “খোলা” না “বন্ধ” — সেই তথ্য কোথাও লেখা নেই।

সমাধানের নীতিটি এক লাইনের: JavaScript শুধু অবস্থা (state) বদলাবে; সেই অবস্থা দেখে চেহারা ঠিক করবে CSS. অবস্থা লেখার তিনটি জায়গা আছে — একটি class (is-open), একটি data-* attribute (data-state="open"), আর যেখানে অবস্থাটি অ্যাক্সেসিবিলিটির জন্যও জরুরি, সেখানে একটি ARIA state. শেষেরটি সবচেয়ে শক্তিশালী, কারণ একই attribute screen reader-কেও খবর দেয় আর CSS-এর attribute selector-কেও। একটি তথ্য, দুই শ্রোতা।

ঢাকার মেট্রো রেলের দরজার ওপরের বাতির কথা ভাবুন। স্টেশন-নিয়ন্ত্রণ শুধু একটি সংকেত পাঠায় — “দরজা খুলছে।” সেই একটি সংকেত দেখেই বাতি জ্বলে, ঘোষণা বাজে, দরজা সরে। নিয়ন্ত্রণকক্ষ বাতির রং বা ঘোষণার ভাষা ঠিক করে না। aria-expanded="true" ঠিক এমন একটি সংকেত: CSS সেটি দেখে চেহারা বদলায়, screen reader সেটি দেখে “expanded” বলে।

<style>
  .menu { display: none; margin: 8px 0; padding: 8px 32px; background: #f1f5f9; }
  [aria-expanded="true"] + .menu { display: block; }
  button[aria-expanded="true"] { background: #1e3a8a; color: white; }
  .arrow { display: inline-block; transition: rotate .2s; }
  [aria-expanded="true"] .arrow { rotate: 180deg; }
</style>
<nav aria-label="প্রধান মেনু">
  <button type="button" aria-expanded="false" aria-controls="m1">
    মেনু <span class="arrow" aria-hidden="true">▼</span>
  </button>
  <ul class="menu" id="m1">
    <li><a href="#home">হোম</a></li>
    <li><a href="#courses">কোর্স</a></li>
    <li><a href="#contact">যোগাযোগ</a></li>
  </ul>
</nav>
<script>
  const toggle = document.querySelector('[aria-expanded]');
  toggle.addEventListener('click', () => {
    const open = toggle.getAttribute('aria-expanded') === 'true';
    toggle.setAttribute('aria-expanded', String(!open));
    console.log('aria-expanded =', toggle.getAttribute('aria-expanded'));
  });
</script>

শুরুতে শুধু “মেনু ▼” বোতাম। চাপলে বোতাম গাঢ় নীল হয়, তীর ঘুরে উল্টে যায়, নিচে তিনটি লিংক খোলে, আর কনসোলে আসে aria-expanded = true. স্ক্রিপ্ট মাত্র একটি attribute বদলাচ্ছে — বাকি সবকিছু CSS-এর [aria-expanded="true"] selector-এর কাজ। চেষ্টা করুন: বোতামে Tab দিয়ে ফোকাস এনে Enter বা Space চাপুন — আসল <button> বলে কি-বোর্ডেও একই click ঘটে। তারপর CSS-এ .menu-কে display: flex; gap: 16px; list-style: none; দিন — মেনুর নকশা পুরো বদলাল, স্ক্রিপ্টে হাতই লাগেনি।

একটি click-এর পর ঠিক কী কী ঘটে, ধাপে ধাপে দেখুন:

  1. ব্যবহারকারীclick / Enterবোতাম চাপলেন
  2. JavaScriptsetAttributeশুধু অবস্থা
  3. DOMaria-expanded="true"একটি সত্য
  4. CSS[aria-expanded="true"]চেহারা
  5. Screen reader“expanded”ঘোষণা
  1. ব্যবহারকারী বোতাম চাপলেন। মাউসে, আঙুলে, কিংবা কি-বোর্ডে Enter/Space — আসল <button> হলে তিন ক্ষেত্রেই ব্রাউজার একই click event পাঠায়। আপনাকে আলাদা করে কি-বোর্ড সামলাতে হয় না।
  2. JavaScript শুধু অবস্থা বদলাল। Event listener বর্তমান মান পড়ে উল্টে দিল: "false" → "true". কোনো রং নয়, কোনো display নয়।
  3. DOM-এ একটি সত্য। এখন পুরো পাতায় “মেনু খোলা কি না” প্রশ্নের একটিমাত্র উত্তর আছে — এই attribute. পরে অন্য কোনো স্ক্রিপ্ট বা আপনি DevTools-এ দেখলেও এখানেই সত্যটা পাবেন।
  4. CSS চেহারা বদলাল। Style recalculation-এ [aria-expanded="true"] + .menu এখন মেলে, তাই মেনু দেখা যায়, বোতাম গাঢ় হয়, তীর ঘোরে। পর্ব ৩৮-এর transition-ও নিজে থেকে কাজ করে।
  5. Screen reader ঘোষণা দিল। ব্রাউজার accessibility tree-ও হালনাগাদ করে, তাই NVDA-র মতো screen reader বলে “মেনু, বোতাম, expanded” (শব্দ আর ক্রম screen reader ভেদে একটু আলাদা হয়). একই attribute থেকে দুই শ্রোতাই খবর পেল — চোখে দেখা মানুষ আর কানে শোনা মানুষ।

দুটি সতর্কতা। প্রথমত, পর্ব ১১-এর কথা মনে রাখুন: খোলা-বন্ধের সাধারণ বাক্সের জন্য <details>, ভাসমান মেনুর জন্য popover আর invoker commands এখন JavaScript ছাড়াই কাজ করে। নিজে স্ক্রিপ্ট লেখার আগে দেখুন HTML নিজেই কাজটি পারে কি না — এটাই ARIA-র প্রথম নিয়মের (পর্ব ১২) আত্মা। দ্বিতীয়ত, ARIA state শুধু তখনই ব্যবহার করুন যখন তার অর্থ সত্যিই খাটে: খোলা-বন্ধে aria-expanded, চাপা-না-চাপা টগলে aria-pressed, ট্যাবে aria-selected. নিছক সাজের অবস্থার জন্য (যেমন “এই কার্ডটি হাইলাইট”) data-state বা class-ই ঠিক।

৪ · JavaScript থেকে CSS variable, আর event-এর ছোট পরিচয়

কখনো অবস্থাটি “খোলা/বন্ধ”-এর মতো দুই ভাগে ভাগ করা যায় না — একটি সংখ্যা, যেমন স্লাইডারের মান বা স্ক্রলের পরিমাণ। তখন class দিয়ে কাজ হয় না। পর্ব ৩৩-এ দেখেছিলেন এর সমাধান: JavaScript শুধু একটি CSS variable-এর মান বদলাবে — el.style.setProperty('--hue', 200) — আর সেই variable কোথায় কীভাবে ব্যবহার হবে, তা ঠিক করবে CSS. পড়তে চাইলে getComputedStyle(el).getPropertyValue('--hue').

এখানে নীতিটা একটু সূক্ষ্ম: setProperty-ও আসলে inline style বসায়। কিন্তু আমরা চেহারা (background, color) বসাচ্ছি না, বসাচ্ছি একটি কাঁচা সংখ্যা। সেই সংখ্যা দিয়ে কী রং হবে, কতটা গাঢ় হবে, dark mode-এ কী হবে — সব সিদ্ধান্ত এখনো CSS-এর হাতে।

<style>
  :root { --hue: 160; }
  .badge {
    display: inline-block; padding: 12px 20px; border-radius: 999px;
    background: hsl(var(--hue) 70% 88%);
    color: hsl(var(--hue) 80% 20%);
    border: 2px solid hsl(var(--hue) 60% 40%);
  }
</style>
<label for="hue">রঙের কোণ (hue):</label>
<input type="range" id="hue" min="0" max="360" value="160">
<output id="out" for="hue">160</output>
<p><span class="badge">ABCL TECH শিক্ষার্থী</span></p>
<script>
  const rootEl = document.documentElement;
  const slider = document.querySelector('#hue');
  slider.addEventListener('input', () => {
    rootEl.style.setProperty('--hue', slider.value);
    document.querySelector('#out').textContent = slider.value;
  });
  const start = getComputedStyle(rootEl).getPropertyValue('--hue');
  console.log('শুরুতে --hue =', start.trim());
</script>

কনসোলে আসে শুরুতে --hue = 160, আর পর্দায় সবুজাভ একটি “ব্যাজ”। স্লাইডার টানলে ব্যাজের পটভূমি, লেখা আর বর্ডার — তিনটিই একসাথে রং বদলায়, কারণ তিনটিই একই --hue থেকে হিসাব হয়। চেষ্টা করুন: CSS-এ color-এর লাইটনেস 20% থেকে 35% করুন — স্ক্রিপ্ট না ছুঁয়েই পুরো ব্যাজের চরিত্র বদলাবে। তারপর 'input'-কে 'change' করে দেখুন: এবার রং বদলাবে কেবল স্লাইডার ছেড়ে দেওয়ার পর।

Event — ব্রাউজারের “ঘটনা ঘটেছে” সংকেত

উপরের দুটি উদাহরণেই একটি লাইন ছিল: addEventListener('click', …). ব্রাউজার প্রতিটি ঘটনার — ক্লিক, টাইপ, স্ক্রল, ফর্ম জমা — একটি event পাঠায়, আর আপনি বলে রাখেন “এই ঘটনা ঘটলে এই ফাংশন চালাও।” HTML-CSS ডেভেলপারের জন্য কয়েকটি event-ই যথেষ্ট:

  • click — বোতাম বা লিংক চাপলে; আসল বোতামে কি-বোর্ডের Enter/Space-ও এর মধ্যে পড়ে।
  • input — ইনপুট ঘরের মান প্রতিবার বদলালে (প্রতিটি অক্ষরে); change — মান বদলানো “শেষ” হলে।
  • submit — ফর্ম জমা দেওয়ার সময় (পর্ব ০৮-০৯-এর built-in validation পাস করার পর)।
  • keydown — নির্দিষ্ট কি চাপলে, যেমন Escape দিয়ে কিছু বন্ধ করা।

পুরোনো টিউটোরিয়ালে প্রায়ই দেখবেন HTML-এর ভেতরেই <button onclick="…">. এটি এখনো চলে, কিন্তু আধুনিক রীতি হলো স্ক্রিপ্টে addEventListener — HTML থাকে পরিষ্কার, একই event-এ একাধিক কাজ জোড়া যায়, আর কঠোর Content Security Policy (যা inline স্ক্রিপ্ট আটকায়) থাকলেও ভাঙে না। Event কীভাবে DOM-গাছ বেয়ে ওপরে ওঠে (bubbling), কীভাবে একটি listener দিয়ে শত element সামলানো যায় (delegation) — এসব JavaScript কোর্সের event-এর পর্বে।

৫ · template — ছাঁচে ঢালা HTML

একটি অনলাইন দোকানে ধরুন ৫০টি পণ্যের কার্ড দেখাতে হবে, আর তালিকাটি আসছে সার্ভার থেকে। প্রতিটি কার্ডের কাঠামো এক — নাম, দাম, ছবি — শুধু লেখা আলাদা। JavaScript-এর ভেতরে লম্বা স্ট্রিং জোড়া দিয়ে HTML বানানো অগোছালো, ভুলপ্রবণ, আর innerHTML-এর কারণে বিপজ্জনক। HTML-এর নিজস্ব সমাধান <template>.

পিঠা বানানোর ছাঁচের কথা ভাবুন। ছাঁচ নিজে পিঠা নয়, খাওয়াও যায় না — কিন্তু প্রতিবার চাল-গুঁড়া ঢাললে একই নকশার পিঠা বেরোয়। <template>-এর ভেতরের HTML ব্রাউজার পার্স করে ঠিকই, কিন্তু পাতায় দেখায় না, ভেতরের ছবি লোড করে না, স্ক্রিপ্ট চালায় না। পুরো জিনিসটি থাকে template.content-এ, একটি DocumentFragment হয়ে — DOM-এর একটি আলগা টুকরো, যা এখনো কোনো পাতার অংশ নয়।

<style>
  #list { list-style: none; padding: 0; display: grid; gap: 8px; max-width: 320px; }
  .item { display: flex; justify-content: space-between; padding: 8px 12px;
          border: 1px solid #cbd5e1; border-radius: 8px; }
  .price { font-weight: bold; color: #0f766e; }
</style>
<h3>আজকের বাজারদর (উদাহরণ)</h3>
<ul id="list"></ul>
<template id="row">
  <li class="item"><span class="label"></span><span class="price"></span></li>
</template>
<script>
  const rows = [['চাল, ১ কেজি', '৳৭৫'], ['মসুর ডাল, ১ কেজি', '৳১১০'], ['আলু, ১ কেজি', '৳৪০']];
  const tpl = document.querySelector('#row');
  for (const [label, cost] of rows) {
    const copy = tpl.content.cloneNode(true);
    copy.querySelector('.label').textContent = label;
    copy.querySelector('.price').textContent = cost;
    document.querySelector('#list').append(copy);
  }
  console.log('template-এর ভেতরে li:', tpl.content.children.length,
              '| পাতায় li:', document.querySelectorAll('#list li').length);
</script>

পর্দায় তিনটি সাজানো সারি — বাঁয়ে পণ্যের নাম, ডানে সবুজ মোটা দাম (দামগুলো উদাহরণ)। কনসোলে template-এর ভেতরে li: 1 | পাতায় li: 3 — ছাঁচ অক্ষত, কপি তিনটি। চেষ্টা করুন: rows তালিকায় ['পেঁয়াজ, ১ কেজি', '৳৬০'] যোগ করুন — HTML না ছুঁয়েই চতুর্থ সারি আসবে। তারপর <template> শব্দটিকে <div hidden> করে দেখুন — tpl.content আর থাকে না, কনসোলে error আসবে; template-এর এই আলাদা “content” জগৎই তার বিশেষত্ব।

পার্স হওয়ার পরtemplate.content — ঘুমন্ত
#document-fragment  (পাতার বাইরে)
└── li.item
    ├── span.label  (খালি)
    └── span.price  (খালি)
document.querySelector('.label') → null
cloneNode(true)ছাঁচের হুবহু কপি
copy = #document-fragment
└── li.item
    ├── span.label
    └── span.price
true = ভেতরের সব শাখাসহ গভীর কপি
textContentলেখা ভরা, এখনো পাতার বাইরে
copy
└── li.item
    ├── span.label → "চাল, ১ কেজি"
    └── span.price → "৳৭৫"
append(copy)পাতায় বসল, fragment খালি
ul#list
└── li.item  ← এখন দেখা যায়, CSS খাটে
copy → (খালি — সন্তানেরা সরে গেছে)
  1. ঘুমন্ত ছাঁচ। Parser <template>-এর ভেতরটা একটি আলাদা, “নিষ্ক্রিয়” ডকুমেন্টের fragment-এ রাখে। তাই পাতার querySelector সেখানে পৌঁছায় না, ভেতরের <img> লোড হয় না, CSS খাটে না।
  2. কপি করা। tpl.content.cloneNode(true) পুরো টুকরোর একটি গভীর কপি বানায়। ছাঁচটি অক্ষত থাকে, তাই বারবার কপি করা যায়।
  3. লেখা ভরা। কপির ভেতরে querySelector চালিয়ে textContent বসানো হলো। কপিটি এখনো পাতার বাইরে, তাই এই পরিবর্তনে ব্রাউজারকে কিছুই নতুন করে আঁকতে হয় না — এটি দ্রুতও।
  4. পাতায় বসানো। append(copy) করলে fragment নিজে পাতায় যায় না — তার সন্তানেরা সরে গিয়ে বসে, fragment খালি হয়ে যায়। এবার CSS খাটে, ছবি লোড হয়, ব্যবহারকারী দেখতে পান।

৬ · Custom element — নিজের HTML ট্যাগ

এ পর্যন্ত আমরা ব্রাউজারের দেওয়া element নিয়ে কাজ করেছি। Web Components হলো ব্রাউজারের নিজস্ব কয়েকটি প্রযুক্তির একটি দল, যা দিয়ে আপনি নিজের element বানাতে পারেন: <user-card>, <bn-number>, <price-tag>. দলের তিন সদস্য:

🏷️ Custom elements

নতুন ট্যাগের নাম আর তার আচরণ (একটি JavaScript class) ব্রাউজারকে নিবন্ধন করানো।

customElements.define()

🧱 Shadow DOM

element-এর ভেতরে একটি লুকানো, আলাদা DOM-গাছ, যার CSS বাইরের পাতা থেকে সুরক্ষিত।

attachShadow()

🧩 template আর slot

ভেতরের কাঠামোর ছাঁচ, আর বাইরে থেকে দেওয়া লেখা কোথায় বসবে তার “ফাঁকা ঘর”।

<template>, <slot>

তিনটি আলাদাভাবেও ব্যবহার করা যায় — shadow ছাড়া custom element, কিংবা custom element ছাড়া template. শুরু করা যাক নামের নিয়ম দিয়ে। একটি custom element-এর নাম:

  • অবশ্যই একটি হাইফেন (-) থাকবে: user-card ঠিক, usercard ভুল।
  • শুরু হবে ছোট হাতের ইংরেজি অক্ষর দিয়ে, আর বড় হাতের ইংরেজি অক্ষর থাকবে না: UserCard বা 1-card চলবে না।
  • কয়েকটি নাম সংরক্ষিত, কারণ SVG আর MathML আগেই নিয়েছে — যেমন font-face, annotation-xml.

হাইফেনের নিয়মটি একটি চুক্তি: HTML নিজে কখনো হাইফেনওয়ালা নামের element যোগ করবে না। তাই আপনার আজকের <tab-list> ভবিষ্যতে HTML-এর কোনো নতুন ট্যাগের সাথে ধাক্কা খাবে না। ব্রাউজারও নামটি দেখেই আচরণ বদলায় — নিচের ছোট স্ক্রিপ্টে দেখুন:

// নামে হাইফেন থাকলে ব্রাউজার ধরে নেয়: "এটি হয়তো পরে define হবে"
const a = document.createElement('user-card');
const b = document.createElement('usercard');
console.log('user-card  ->', a.constructor.name);
console.log('usercard   ->', b.constructor.name);

class UserCard extends HTMLElement {}
customElements.define('user-card', UserCard);
console.log('after define ->', document.createElement('user-card').constructor.name);

try { customElements.define('usercard', class extends HTMLElement {}); }
catch (e) { console.log('usercard define ->', e.name); }
try { customElements.define('user-card', class extends HTMLElement {}); }
catch (e) { console.log('second define ->', e.name); }

প্রথম দুই লাইনে user-card -> HTMLElement আর usercard -> HTMLUnknownElement. হাইফেনওয়ালা নামকে ব্রাউজার “সম্ভাব্য custom element” ধরে অপেক্ষায় রাখে; হাইফেন ছাড়া অচেনা নাম সরাসরি “অজানা”। define-এর পর নতুন element হয় UserCard. হাইফেন ছাড়া নাম define করতে গেলে SyntaxError, আর একই নাম দুবার define করলে NotSupportedError — নাম-নিবন্ধন পুরো পাতায় একটিই, আর একবারই। চেষ্টা করুন: 'usercard'-কে 'User-card' করুন — বড় হাতের অক্ষরের জন্যও একই SyntaxError আসবে।

Lifecycle callback — element-এর জীবনের মুহূর্তগুলো

Custom element-এর class-এ কয়েকটি বিশেষ নামের method লিখলে ব্রাউজার নিজেই ঠিক সময়ে সেগুলো ডাকে; এদের বলে lifecycle callback:

  • constructor() — element তৈরি বা “উন্নীত” (upgrade) হওয়ার মুহূর্তে। শুরুতে super() বাধ্যতামূলক; এখানে attribute পড়া বা element-এ সরাসরি সন্তান যোগ করা উচিত নয় (তবে নিজের shadow root বানিয়ে তার ভেতরে কাঠামো বসানো চলে — অংশ ৭-এর উদাহরণের মতো)।
  • connectedCallback() — element পাতায় বসলে। বেশিরভাগ কাজ — event জোড়া, ভেতরের কাঠামো সাজানো — এখানেই।
  • disconnectedCallback() — পাতা থেকে সরালে; টাইমার বা বাইরের listener পরিষ্কারের জায়গা।
  • attributeChangedCallback(name, old, new) — static observedAttributes-এ লেখা কোনো attribute বদলালে।
<p>মোট ভর্তি: <bn-number value="1250"></bn-number> জন</p>
<button type="button" id="add">আরও একজন ভর্তি</button>
<script>
  class BnNumber extends HTMLElement {
    static observedAttributes = ['value'];
    constructor() { super(); console.log('constructor'); }
    attributeChangedCallback(attr, oldV, newV) {
      console.log('attributeChanged:', attr, oldV, '->', newV);
      this.render();
    }
    connectedCallback() { console.log('connectedCallback — পাতায় বসল'); }
    render() {
      const bn = '০১২৩৪৫৬৭৮৯';
      this.textContent = (this.getAttribute('value') || '').replace(/[0-9]/g, d => bn[d]);
    }
  }
  customElements.define('bn-number', BnNumber);
  document.querySelector('#add').addEventListener('click', () => {
    const el = document.querySelector('bn-number');
    el.setAttribute('value', Number(el.getAttribute('value')) + 1);
  });
</script>

পর্দায় “মোট ভর্তি: ১২৫০ জন” — HTML-এ 1250 লেখা, element নিজেই বাংলা অঙ্কে দেখাচ্ছে। কনসোলে ক্রমটি খেয়াল করুন: constructor, তারপর attributeChanged: value null -> 1250, শেষে connectedCallback. বোতাম চাপলে শুধু attributeChanged: value 1250 -> 1251 আসে আর লেখা হয় ১২৫১. চেষ্টা করুন: observedAttributes থেকে 'value' মুছে দিন — বোতাম চাপলে attribute বদলাবে (DevTools-এ দেখা যাবে), কিন্তু element আর খবর পাবে না, লেখাও বদলাবে না।

পাতায় আগে থেকে থাকা element-এর গল্পটা একটু মজার। Parser যখন <bn-number> পায়, তখনো define হয়নি — element-টি একটি সাধারণ, “অসংজ্ঞায়িত” element হয়ে বসে থাকে। Define হওয়ামাত্র ব্রাউজার তাকে upgrade করে। ধাপগুলো দেখুন:

1<bn-number value="1250"></bn-number>2<script>3class BnNumber extends HTMLElement {4  static observedAttributes = ['value'];5  constructor() { super(); }6  attributeChangedCallback(n, o, v) { … }7  connectedCallback() { … }8  disconnectedCallback() { … }9}10customElements.define('bn-number', BnNumber);11el.setAttribute('value', '1251');12el.remove();
element-এর অবস্থা
:defined
value
পাতায় আছে
  1. Parser element পেল। নামে হাইফেন আছে, কিন্তু কেউ এখনো define করেনি। element-টি সাধারণ HTMLElement হয়ে পাতায় বসে; ভেতরে কিছুই নেই। CSS-এর bn-number:not(:defined) selector এই অবস্থাটি ধরতে পারে।
  2. Define ডাকা হলো। এখন ব্রাউজার জানে bn-number মানে BnNumber class. পাতায় এই নামের যত element আছে, সবগুলোর upgrade শুরু হয়।
  3. constructor চলল। element-টি এখন আসল BnNumber, তাই :defined সত্য হলো। এখানে শুধু প্রস্তুতি — attribute পড়া বা সন্তান যোগ করা নয়।
  4. আগে থেকে থাকা attribute-এর খবর। HTML-এ value="1250" আগেই লেখা ছিল, তাই upgrade-এর সময় ব্রাউজার তা “নতুন” হিসেবে জানায় — পুরোনো মান null. element এখানেই বাংলা অঙ্কে লেখা বসায়।
  5. connectedCallback. element পাতাতেই আছে, তাই এবার এটি ডাকা হলো। নতুন element append() করলেও ঠিক এই মুহূর্তে এটি চলে।
  6. attribute বদল। স্ক্রিপ্ট value বদলাল; observedAttributes-এ নামটি আছে বলে আবার attributeChangedCallback. attribute-ই এখানে element-এর “অবস্থা” — অংশ ৩-এর নীতির সাথে হুবহু মেলে।
  7. বিদায়। el.remove() করলে disconnectedCallback. কোনো টাইমার বা window-এ জোড়া listener থাকলে এখানে বন্ধ না করলে সেগুলো চলতেই থাকবে — মেমরি নষ্ট।

Web Components-এর ধারণা প্রথম বড় পরিসরে তুলে ধরেন Google-এর Alex Russell, ২০১১ সালে একটি সম্মেলনে। প্রথম সংস্করণ (“v0”) Chrome-এ এলেও অন্য ব্রাউজার নির্মাতারা নকশায় একমত ছিলেন না; তার একটি অংশ — HTML Imports — শেষ পর্যন্ত বাদই পড়ে, আর তার জায়গা নেয় JavaScript module. ২০১৬ সালের দিকে সবাই নতুন “v1” নকশায় একমত হন। ২০১৮ সালে Firefox আর ২০২০ সালে Chromium-ভিত্তিক নতুন Edge আসার পর custom element আর Shadow DOM সব প্রধান ব্রাউজারে পৌঁছায় — আজ দুটিই Baseline Widely available. পুরোনো টিউটোরিয়াল বা AI-এর উত্তরে document.registerElement() বা createShadowRoot() দেখলে বুঝবেন সেটি বাতিল v0-এর কোড।

৭ · Shadow DOM — CSS-এর দেয়াল আর দেয়ালের দরজা

পর্ব ৪৬-এ দেখেছিলেন, বড় প্রজেক্টে CSS-এর সবচেয়ে বড় মাথাব্যথা হলো সংঘর্ষ — আপনার .title আর অন্য কারও .title একে অপরের নকশা ভাঙে। BEM-এর মতো নামকরণ একটি ভদ্রলোকের চুক্তি; @scope (পর্ব ৩৭) একটি সীমানা। Shadow DOM হলো সত্যিকারের দেয়াল।

this.attachShadow({ mode: 'open' }) লিখলে element-টি একটি shadow host হয়ে যায়, আর তার সাথে জোড়া লাগে একটি shadow root — একটি আলাদা DOM-গাছ, যা পর্দায় element-এর ভেতরে আঁকা হয়, কিন্তু পাতার সাধারণ গাছের অংশ নয়। পাতার নিজের গাছকে তখন বলা হয় light DOM. দেয়ালের নিয়ম:

  • পাতার CSS selector ভেতরে ঢোকে না: পাতায় p { color: crimson } থাকলেও shadow-এর ভেতরের <p> লাল হয় না।
  • ভেতরের CSS বাইরে যায় না: shadow-এর ভেতরের p { … } পাতার অন্য কোনো <p>-কে ছোঁয় না।
  • পাতার querySelector-ও ভেতরে পৌঁছায় না (তবে open হলে el.shadowRoot দিয়ে ইচ্ছা করে ঢোকা যায়)।

কিন্তু পুরো বন্ধ দেয়াল কাজের নয় — বাইরের পাতা অন্তত ফন্ট আর ব্র্যান্ডের রং বলে দিতে চায়। তাই দেয়ালে তিনটি দরজা রাখা আছে:

কীদেয়াল পেরোয়?উদাহরণ
পাতার সাধারণ selectorনাp { color: crimson } ভেতরে খাটে না
উত্তরাধিকারী property (inheritance)হ্যাঁ, host থেকেfont-family, color, line-height
CSS custom propertyহ্যাঁ, উত্তরাধিকারেprice-card { --accent: #7c3aed }
::part()হ্যাঁ, শুধু যে অংশে part="…" আছেprice-card::part(price) { … }
host element-এর নিজের স্টাইলবাইরে থেকে দেওয়া যায়, আর বাইরেরটাই জেতেprice-card { margin: 0 } ভেতরের :host-কে হারায়

ভেতর থেকে host element-কে সাজানোর selector হলো :host (আর শর্তসহ :host(.compact) লেখা যায়)। মনে রাখুন, custom element ডিফল্টে display: inline, তাই কার্ডের মতো জিনিসে :host { display: block } প্রায় সবসময় লাগে।

<style>
  /* পাতার CSS */
  body { font-family: system-ui, sans-serif; }
  p { color: crimson; font-weight: bold; }
  price-card { --accent: #7c3aed; }
  price-card::part(price) { font-size: 1.6em; }
</style>
<p>পাতার সাধারণ অনুচ্ছেদ — লাল আর মোটা।</p>
<price-card></price-card>
<script>
  class PriceCard extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML = `
        <style>
          :host { display: block; max-width: 260px; margin-top: 12px; padding: 4px 16px;
                  border: 2px solid var(--accent, teal); border-radius: 12px; }
          [part="price"] { color: var(--accent, teal); }
        </style>
        <p>Shadow-এর ভেতরের অনুচ্ছেদ</p>
        <p part="price">৳১,২০০</p>`;
    }
  }
  customElements.define('price-card', PriceCard);
  console.log('বাইরের p:', document.querySelectorAll('p').length, 'টি — ভেতরেরগুলো গোনায় নেই');
</script>

ওপরের অনুচ্ছেদ লাল ও মোটা — পাতার CSS. কিন্তু কার্ডের ভেতরের “Shadow-এর ভেতরের অনুচ্ছেদ” কালো, সাধারণ — পাতার p নিয়ম দেয়াল পেরোয়নি। অথচ ফন্ট পাতার মতোই (font-family উত্তরাধিকারে ঢুকেছে), বর্ডার আর দাম বেগুনি (--accent পেরিয়েছে), আর দাম বড় (::part(price)-এর কাজ)। কনসোলে বাইরের p: 1 — ভেতরের দুটি <p> পাতার querySelectorAll-এ গোনাই হয়নি। চেষ্টা করুন: পাতার CSS-এ --accent-কে #dc2626 করুন — পুরো কার্ড লাল থিমে যাবে। তারপর পাতার CSS-এ price-card p { color: blue } লিখে দেখুন — কিছুই বদলাবে না; দেয়ালের ভেতরে selector ঢোকে না।

আরও গভীরে: Shadow DOM ব্রাউজারের কাছে নতুন কিছু নয় — ব্রাউজার বহু বছর ধরে নিজের কন্ট্রোল এভাবেই বানায়। <input type="range">-এর হাতল, <video controls>-এর বোতামগুলো আসলে একটি “user-agent shadow root”-এর ভেতরে থাকে; Chrome বা Edge-এর DevTools-এর Settings → Preferences-এ “Show user agent shadow DOM” চালু করলে নিজেই দেখতে পাবেন। Web Components সেই একই ক্ষমতা সাধারণ ডেভেলপারের হাতে তুলে দিয়েছে। Cascade-এর দিক থেকে spec একে বলে আলাদা “encapsulation context”: বাইরের আর ভেতরের নিয়ম মুখোমুখি হলে সাধারণ ঘোষণায় বাইরেরটা জেতে, আর !important-এ উল্টো — ভেতরেরটা। এজন্যই :host-এর স্টাইলকে “ডিফল্ট” বলা হয়, যা পাতা চাইলে বদলাতে পারে। আর অনেক component-এ একই স্টাইল বারবার না পাঠাতে adoptedStyleSheets (constructable stylesheet) দিয়ে একটি stylesheet অনেক shadow root-এ ভাগ করে দেওয়া যায় — এটি ২০২৩ সাল থেকে সব প্রধান ব্রাউজারে আছে।

৮ · slot আর declarative shadow DOM — JavaScript ছাড়াই shadow

একটি component-এর কাঠামো আপনার, কিন্তু ভেতরের লেখা দেবেন ব্যবহারকারী — যেমন একটি প্রোফাইল কার্ডে নাম আর পেশা। এই “ফাঁকা ঘর”-এর নাম <slot>. Shadow-এর ভেতরে <slot name="title"> রাখলে, host-এর ভেতরে slot="title" লেখা যে element আছে, সেটি পর্দায় ওই জায়গায় দেখা যায়। নাম ছাড়া <slot> নেয় বাকি সবকিছু, আর slot-এর ভেতরের লেখা হলো fallback — বাইরে থেকে কিছু না এলে সেটি দেখায়।

গুরুত্বপূর্ণ একটি সূক্ষ্মতা: slot-এ বসা element সরে যায় না। সেটি light DOM-এই থাকে, শুধু পর্দায় অন্য জায়গায় আঁকা হয়। তাই পাতার CSS তাকে স্বাভাবিকভাবে সাজাতে পারে। ভেতর থেকে সাজাতে আছে ::slotted(), তবে সে শুধু slot-এ সরাসরি বসা element-কে ধরে (তার ভেতরের নাতি-নাতনিকে নয়), আর দুই পক্ষে টানাটানি হলে পাতার CSS জেতে।

Declarative shadow DOM

এতক্ষণ shadow root বানাতে JavaScript লেগেছে। ফলে সার্ভারে বানানো পাতায় একটি সমস্যা: স্ক্রিপ্ট চলার আগে component খালি বা অগোছালো দেখায়, আর স্ক্রিপ্ট বন্ধ থাকলে কিছুই দেখায় না। সমাধান declarative shadow DOM: host-এর ভেতরে (সাধারণত প্রথম সন্তান হিসেবে) <template shadowrootmode="open"> লিখলে parser নিজেই সেই template-কে shadow root বানিয়ে দেয় — এক লাইন JavaScript ছাড়াই।

<style>
  /* বাইরের CSS — slot-এ বসা light DOM-কে এখনো ছুঁতে পারে */
  .role { color: #0f766e; }
</style>
<user-card>
  <template shadowrootmode="open">
    <style>
      :host { display: block; max-width: 300px; padding: 12px 16px;
              border: 1px solid #cbd5e1; border-radius: 12px; }
      ::slotted(h3) { margin: 0; }
      ::slotted(.role) { font-style: italic; }
      footer { color: #64748b; font-size: .85em; }
    </style>
    <slot name="title"></slot>
    <slot></slot>
    <footer><slot name="note">কোনো নোট নেই</slot></footer>
  </template>
  <h3 slot="title">নুসরাত জাহান</h3>
  <p class="role">ফ্রন্ট-এন্ড ডেভেলপার, রাজশাহী</p>
</user-card>
<script>
  const host = document.querySelector('user-card');
  const main = host.shadowRoot.querySelector('slot:not([name])');
  console.log('shadowRoot আছে?', host.shadowRoot !== null,
              '| নামহীন slot-এ', main.assignedElements().length, 'টি element');
</script>

একটি বর্ডারওয়ালা কার্ড: ওপরে “নুসরাত জাহান” (নাম কাল্পনিক), নিচে তির্যক সবুজ লেখায় পেশা, আর শেষে ধূসর “কোনো নোট নেই” — note-slot-এ কিছু দেওয়া হয়নি বলে fallback দেখাচ্ছে। সবুজ রং এসেছে বাইরের .role নিয়ম থেকে, তির্যক ভাব ভেতরের ::slotted(.role) থেকে — দুই পক্ষই slot-এ বসা element-কে সাজাতে পারে। কনসোলে shadowRoot আছে? true | নামহীন slot-এ 1 টি element. লক্ষ করুন, user-card কোথাও define-ই হয়নি, তবু shadow তৈরি — এটি parser-এর কাজ। চেষ্টা করুন: কার্ডের ভেতরে <small slot="note">সদস্য ২০২৪ থেকে</small> যোগ করুন — fallback সরে গিয়ে আপনার লেখা বসবে। তারপর shadowrootmode attribute মুছে দিন — template আবার সাধারণ, অদৃশ্য template হয়ে যাবে, আর শুধু light DOM-এর লেখা দেখাবে।

সাপোর্ট-অবস্থা

<template>, custom element, Shadow DOM, <slot>, :host, ::slotted(), ::part() আর :defined — সবই Baseline Widely available. Declarative shadow DOM (shadowrootmode) ২০২৪ সালের শুরুতে Firefox যোগ দেওয়ার পর Baseline Newly available হয়; Baseline-এর ৩০ মাসের হিসাবে ২০২৬ সালের মাঝামাঝি এটি Widely available-এর সীমা পার হওয়ার কথা — নিশ্চিত হতে web.dev-এর Baseline পাতা বা Can I use দেখে নিন। পুরোনো ব্রাউজারে এটি শুধু একটি অদৃশ্য template থাকে, তাই light DOM-এর আসল লেখা (নাম, পেশা) তবু দেখা যায় — ভালো একটি fallback. Chrome-এর প্রথম সংস্করণে attribute-টির নাম ছিল shadowroot; পুরোনো লেখায় সেটি দেখলে আজকের নাম shadowrootmode ব্যবহার করুন।

একটি ফাঁদ জেনে রাখুন: declarative shadow DOM শুধু parser-এর পথে কাজ করে। innerHTML দিয়ে এমন HTML বসালে template শুধু template-ই থাকে।

// innerHTML দিলে declarative shadow DOM কাজ করে না — template থেকে যায় template হয়েই
box.innerHTML = '<x-card><template shadowrootmode="open">…</template></x-card>';

// HTML parser-এর পূর্ণ নিয়মে পার্স করতে নতুন method (নামেই সতর্কবার্তা: ইনপুট নিরাপদ হতে হবে)
box.setHTMLUnsafe('<x-card><template shadowrootmode="open">…</template></x-card>');

৯ · অ্যাক্সেসিবিলিটি, progressive enhancement আর framework-এর প্রশ্ন

Custom element কোনো বাটন নয়

<fancy-button> নামটি যত সুন্দরই হোক, ব্রাউজারের কাছে এটি একটি <span>-এর মতোই অর্থহীন। এর কোনো role নেই, Tab চাপলে ফোকাস আসে না, Enter-এ কিছু ঘটে না, ফর্মের সাথে যুক্ত নয়। পর্ব ১১-এর div-বাটনের সব সমস্যা হুবহু ফিরে আসে — শুধু নামটা নতুন।

<!-- ভুল: দেখতে বাটন, কিন্তু role নেই, Tab-এ ফোকাস হয় না, Enter-এ কাজ করে না -->
<fancy-button onclick="save()">সেভ করুন</fancy-button>

<!-- ঠিক: custom element শুধু মোড়ক, ভেতরে আসল button -->
<fancy-button>
  <button type="button">সেভ করুন</button>
</fancy-button>
  • ভেতরে native element রাখুন। বাটনের কাজে <button>, লিংকে <a href>, ইনপুটে <input> — role, কি-বোর্ড ফোকাস, accessible name, ফর্মে জমা — সব বিনা পরিশ্রমে আসে।
  • Shadow-এর দেয়াল ARIA-র id-কেও আটকায়। বাইরের <label for="x"> বা aria-labelledby shadow-এর ভেতরের id খুঁজে পায় না। তাই label আর input একই দিকে রাখুন, নয়তো light DOM-এ রেখে slot ব্যবহার করুন। (এই সমস্যা সমাধানের একটি প্রস্তাব নিয়ে কাজ চলছে, এখনো Baseline নয়।)
  • ফোকাস ভেতরে পাঠাতে attachShadow({ mode: 'open', delegatesFocus: true }) — host-এ ফোকাস দিলে ভেতরের প্রথম ফোকাসযোগ্য element পায়।
  • নিজস্ব ফর্ম-কন্ট্রোল বানাতে হলে ElementInternals (attachInternals(), static formAssociated = true) দিয়ে মান ফর্মে পাঠানো আর role দেওয়া যায়; এটি ২০২৩ সাল থেকে সব প্রধান ব্রাউজারে আছে। তবু কি-বোর্ডের আচরণ নিজেকেই লিখতে হয় — তাই সম্ভব হলে native element-কেই মুড়ে দিন।

HTML web components — যা আছে তাকে উন্নত করা

অংশ ৮ পর্যন্ত আমাদের component-গুলো নিজেই সব কাঠামো বানিয়েছে; স্ক্রিপ্ট না চললে পাতায় ফাঁকা জায়গা। ২০২৩ সালের দিকে Jeremy Keith-সহ কয়েকজন ওয়েব ডেভেলপার একটি ভিন্ন ধরনকে “HTML web components” নামে জনপ্রিয় করেন: custom element-এর ভেতরে আগে থেকেই কাজ করা সাধারণ HTML থাকবে, আর component শুধু বাড়তি সুবিধা যোগ করবে। স্ক্রিপ্ট দেরিতে এলে, ব্যর্থ হলে বা বন্ধ থাকলেও মূল কাজ চলে — পর্ব ৪৮-এর progressive enhancement-এর সরাসরি প্রয়োগ। প্রায়ই এসব component-এ Shadow DOM-ই লাগে না, তাই পাতার CSS সরাসরি সাজাতে পারে।

<style>
  textarea { display: block; width: 100%; max-width: 320px; }
  char-count output { font-size: .9em; color: #475569; }
  char-count output.near { color: #b45309; font-weight: bold; }
</style>
<char-count>
  <label for="sms">SMS-এর লেখা</label>
  <textarea id="sms" maxlength="70" rows="3"></textarea>
</char-count>
<script>
  customElements.define('char-count', class extends HTMLElement {
    connectedCallback() {
      const box = this.querySelector('textarea');
      if (!box) return;                         // যা উন্নত করব, তা না থাকলে কিছুই নয়
      const max = Number(box.getAttribute('maxlength'));
      const out = document.createElement('output');
      this.append(out);
      const update = () => {
        const left = max - box.value.length;
        out.textContent = 'আর ' + left.toLocaleString('bn-BD') + 'টি অক্ষর লেখা যাবে';
        out.classList.toggle('near', left <= 10);
      };
      box.addEventListener('input', update);
      update();
      console.log('textarea আগেই কাজ করত — এখন শুধু গোনার সুবিধা যোগ হলো');
    }
  });
</script>

একটি সাধারণ label আর textarea, নিচে “আর ৭০টি অক্ষর লেখা যাবে” (বাংলা অঙ্কে)। লিখতে থাকলে সংখ্যা কমে, শেষ ১০টিতে লেখা মোটা কমলা হয়; কনসোলে textarea আগেই কাজ করত — এখন শুধু গোনার সুবিধা যোগ হলো. বাংলা SMS-এ এক বার্তায় ৭০টি অক্ষর ধরে, তাই উদাহরণটি বাস্তব — তবে যুক্তাক্ষর ভেতরে কয়েকটি অক্ষর দিয়ে তৈরি বলে (পর্ব ১৪) “ক্ষ” লিখলে তিনটি কমে। <output>-এর নিজস্ব role হলো status, যা একটি ভদ্র live region — screen reader কাজ থামিয়ে নয়, ফাঁকে ফাঁকে সংখ্যাটি জানায়। চেষ্টা করুন: পুরো <script> মুছে দিন — গোনা চলে যাবে, কিন্তু লেখা, label, maxlength সবই কাজ করবে। এটাই HTML web component-এর মূল প্রতিশ্রুতি।

Web Components না framework — কখন কোনটি

React (২০১৩), Vue (২০১৪), Svelte-এর মতো JavaScript framework-ও component দিয়েই কাজ করে। তাহলে দুটির পার্থক্য কোথায়?

🧱 Web Components

ব্রাউজারের নিজস্ব মানদণ্ড — কোনো লাইব্রেরি নামাতে হয় না, বহু বছর পরেও চলবে, আর যেকোনো framework বা সাধারণ HTML পাতায় বসানো যায়। দুর্বলতা: নিচু স্তরের API, অবস্থা (state) সামলানো আর ডেটা থেকে বারবার আঁকার কাজ নিজেকে করতে হয়।

design system, widget, CMS-এর পাতা

⚛️ Framework

ডেটা বদলালে পর্দা নিজে হালনাগাদ করা, রাউটিং, সার্ভারে রেন্ডার, বিশাল লাইব্রেরি-সংসার। দুর্বলতা: বাড়তি JavaScript নামাতে হয়, আর এক framework-এর component অন্যটিতে সহজে চলে না; কয়েক বছরে সংস্করণ বদলে কোড পুরোনো হয়ে যায়।

জটিল অ্যাপ, ড্যাশবোর্ড, SPA

বাস্তবে প্রশ্নটা প্রায়ই “একটি না অন্যটি” নয়। বড় প্রতিষ্ঠান তাদের design system-এর বোতাম, ইনপুট আর কার্ড Web Components-এ বানায়, যাতে React, Vue বা সাধারণ HTML — সব দলের পাতায় একই component চলে; Adobe, Salesforce আর Microsoft-এর design system-এ এর উদাহরণ আছে, আর GitHub-এর পাতায় <relative-time>-এর মতো ছোট custom element ব্যবহার হয়। Google-এর Lit-এর মতো ছোট লাইব্রেরি Web Components লেখাকে framework-এর মতো আরামদায়ক করে। আর React ১৯ সংস্করণ থেকে custom element-কে পূর্ণ সমর্থন দেয়। আপনার সিদ্ধান্তের সহজ নিয়ম: একটি স্বয়ংসম্পূর্ণ widget, যা অনেক জায়গায় চলবে — Web Component; ডেটায় ঠাসা পুরো অ্যাপ — framework. দুই দিকের তুলনা বিস্তারিত দেখতে JavaScript কোর্সের React, Vue ও Svelte-এর পর্বটি পড়ুন; পর্ব ৫১-এ দেখা CSS framework-এর প্রশ্নটিও এর সাথে মেলে — যা নামাবেন, তার দাম দিতে হবে।

১০ · AI-এর যুগে — AI-এর লেখা DOM কোড আর component যাচাই

“একটা dropdown মেনু বানিয়ে দাও” — AI মুহূর্তেই কোড দেবে, প্রিভিউতে চলবেও। কিন্তু AI শিখেছে বহু বছরের পুরোনো কোড থেকে, যার বড় অংশ jQuery-যুগের অভ্যাসে লেখা। তাই প্রায়ই এমন কোড আসে:

// AI-এর পুরোনো ধাঁচ: চেহারা সরাসরি JavaScript-এ
btn.onclick = function () {
  if (menu.style.display === 'block') {
    menu.style.display = 'none';
    btn.style.background = '';
  } else {
    menu.style.display = 'block';
    btn.style.background = '#1e3a8a';
    btn.style.color = 'white';
  }
};

চলে, কিন্তু রং আর display JavaScript-এ আটকে গেছে, CSS-এর media query বা dark mode এগুলোকে হারাতে পারবে না, আর screen reader জানেই না মেনু খোলা কি না। অংশ ৩-এর মতো aria-expanded আর CSS selector-এ নিলে একই কাজ অর্ধেক কোডে, সবার জন্য হয়।

AI সাধারণত যেসব ভুল করে

AI-এর ভুলকেন সমস্যাঠিক উপায়
el.style.display/color দিয়ে চেহারাCSS হারে, নকশা দুই জায়গায়class, data-*, aria-* বদল; চেহারা CSS-এ
ব্যবহারকারীর লেখা innerHTML-এXSS-এর দরজা খোলেtextContent, <template>
<div onclick> বা <my-button>-কে বাটন বানানোrole নেই, কি-বোর্ডে চলে নাভেতরে আসল <button>
হাইফেন ছাড়া নাম, বা একই নাম দুবার defineSyntaxError / NotSupportedErroruser-card; দরকারে customElements.get() দিয়ে আগে যাচাই
registerElement, createShadowRoot, HTML Imports, /deep/বাতিল v0-যুগের APIcustomElements.define, attachShadow, ::part()
shadowroot="open"পুরোনো, অমানক নামshadowrootmode="open"
“closed shadow root দিলে কোড নিরাপদ”এটি নিরাপত্তা নয়, শুধু আড়ালগোপন তথ্য কখনো ব্রাউজারের কোডে নয়
সাধারণ খোলা-বন্ধের জন্য পুরো স্ক্রিপ্টঅপ্রয়োজনীয় JavaScript<details>, popover, <dialog> (পর্ব ১১)

AI-কে কাজ দেওয়ার সময় নীতিগুলো আগেই লিখে দিন। শেষ লাইনটি খেয়াল করুন — AI-কে নিজের কোড স্ক্রিপ্ট ছাড়া চালিয়ে ভাবতে বললে progressive enhancement-এর ফাঁক প্রায়ই নিজেই বেরিয়ে আসে।

তুমি একজন অ্যাক্সেসিবিলিটি-সচেতন ফ্রন্ট-এন্ড ডেভেলপার। আমার অনলাইন দোকানের পণ্যের পাতার জন্য একটি "আরও পড়ুন" ধরনের component বানাও, নাম read-more. শর্ত: ১. এটি একটি HTML web component হবে: ভেতরে আগে থেকেই পুরো লেখা থাকবে, স্ক্রিপ্ট না চললে পুরো লেখাই দেখা যাবে। ২. স্ক্রিপ্ট চললে লেখা ছোট হবে আর ভেতরে একটি আসল button যোগ হবে, যার aria-expanded আর aria-controls ঠিক থাকবে। ৩. JavaScript কোনো রং, display বা উচ্চতা বসাবে না; শুধু aria-expanded বদলাবে। সব চেহারা আলাদা CSS-এ, [aria-expanded] selector দিয়ে। ৪. innerHTML ব্যবহার করবে না; customElements.define দেবে, নামে হাইফেন রাখবে, আর একই নাম দুবার define হওয়া আটকাবে। ৫. Shadow DOM দরকার না হলে ব্যবহার করবে না, আর কেন করলে না বা করলে, এক লাইনে বলবে। শেষে সহজ বাংলায় বলো: JavaScript বন্ধ থাকলে পাতাটি কেমন দেখাবে, আর screen reader বোতামে গেলে কী শোনাবে।

AI-এর দেওয়া DOM কোড বা component হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ফ্রন্ট-এন্ড ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন প্রায়ই আসে:

  • DOM কী, আর HTML ফাইলের সাথে তার পার্থক্য? — HTML থেকে ব্রাউজারের বানানো জীবন্ত গাছ; JavaScript এটি বদলায়, ফাইল বদলায় না।
  • Web Components-এর তিনটি মূল প্রযুক্তি কী? — custom elements, Shadow DOM, আর template/slot.
  • Custom element-এর নামে হাইফেন কেন বাধ্যতামূলক? — ভবিষ্যতের HTML ট্যাগের সাথে সংঘর্ষ এড়াতে; HTML কখনো হাইফেনওয়ালা নাম নেবে না।
  • Shadow DOM-এর ভেতরে বাইরে থেকে স্টাইল দেওয়ার উপায়? — উত্তরাধিকারী property, CSS custom property আর ::part().
  • el.style না classList — কোনটি, আর কেন? — classList বা aria/data attribute; inline style CSS-কে হারায়, আর নকশা দুই জায়গায় ছড়ায়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-কপিকারী থেকে ওয়েব প্ল্যাটফর্ম-চিন্তক বানাবে।

প্র ০১Web Components-এর ধারণা এসেছিল ২০১১ সালে, React ২০১৩ সালে — তবু এক দশক ধরে জনপ্রিয়তায় framework এগিয়ে থাকল কেন?

কারণটা প্রযুক্তির চেয়ে বেশি সময় আর সমস্যার মাপের। ২০১১ সালে Alex Russell যখন ধারণাটি তুলে ধরেন, তখন সেটি একটি প্রস্তাব মাত্র। Chrome “v0” সংস্করণ চালু করলেও Firefox আর Safari-র নির্মাতারা নকশার কয়েকটি অংশে আপত্তি তোলেন — বিশেষ করে HTML Imports, যা JavaScript module আসার পর অপ্রয়োজনীয় মনে হয়। আলোচনা, নতুন নকশা (“v1”), আর সব ব্রাউজারে পৌঁছাতে প্রায় ২০২০ সাল লেগে যায়। ততদিনে ডেভেলপারদের পুরো এক প্রজন্ম React-এ অভ্যস্ত।

দ্বিতীয় কারণ, দুটি আসলে একই সমস্যার সমাধান নয়। React-এর মূল আকর্ষণ ছিল “ডেটা বদলালে পর্দা নিজে মিলে যাবে” — অবস্থা সামলানো আর বারবার আঁকার একটি সুন্দর চিন্তাকাঠামো। Web Components দেয় নিচু স্তরের উপাদান: নতুন ট্যাগ আর স্টাইলের দেয়াল। ডেটা থেকে পর্দা আঁকার কাজ আপনাকে নিজে করতে হয়। অনেক দলের কাছে এটি “অর্ধেক framework” মনে হয়েছে। Google-এর Polymer, পরে Lit, এই ফাঁক ভরাট করতে চেয়েছে; ২০১৭ সালে YouTube-এর নতুন নকশা Polymer-এ তৈরি হয়েছিল — অর্থাৎ বড় প্রয়োগও ছিল।

তৃতীয় কারণ, সংসার। React-এর চারপাশে গড়ে উঠল বিশাল লাইব্রেরি, টিউটোরিয়াল, চাকরির বাজার। চাকরির বিজ্ঞাপনে React চাওয়া হয়, তাই মানুষ React শেখে, তাই কোম্পানিও React বেছে নেয় — একটি নিজে-চলা চক্র।

তবে ছবি বদলাচ্ছে। Design system-এর মতো জায়গায়, যেখানে একই বোতাম React, Vue আর সাধারণ পাতায় চলতে হবে, Web Components-ই স্বাভাবিক উত্তর। React ১৯ custom element-কে পূর্ণ সমর্থন দেওয়ায় দুটি একসাথে চালানো আরও সহজ হয়েছে। আর ব্রাউজারের মানদণ্ড বলে Web Components-এর কোড বহু বছর পরেও চলে — framework-এর সংস্করণ বদলের ঝক্কি নেই।

মূল উপলব্ধি: প্রযুক্তি জেতে শুধু ভালো হলে নয় — ঠিক সময়ে, ঠিক সমস্যার সমাধান হলে, আর তার চারপাশে মানুষের সংসার গড়ে উঠলে। প্ল্যাটফর্মের মানদণ্ড ধীরে আসে, কিন্তু অনেক দিন টেকে।

প্র ০২Shadow DOM-এর দেয়াল আশীর্বাদ না অভিশাপ? যে কোম্পানি পুরো সাইটের থিম এক জায়গা থেকে বদলাতে চায়, তার জন্য এটি কী মানে?

দুটোই — কোন দিক থেকে দেখছেন তার ওপর নির্ভর করে। Component-নির্মাতার কাছে আশীর্বাদ: ধরুন bKash-এর মতো একটি পেমেন্ট প্রতিষ্ঠান অন্য ওয়েবসাইটে বসানোর জন্য একটি “পেমেন্ট বোতাম” component দিল। হাজারো সাইটের হাজারো রকম CSS — কারও button { all: unset }, কারও * { font-size: 12px }. Shadow DOM ছাড়া প্রতিটি সাইট বোতামটি ভাঙতে পারত; দেয়ালের কারণে বোতাম সবখানে একরকম দেখায়।

পাতার মালিকের কাছে মাথাব্যথা: যে কোম্পানি একটি global stylesheet দিয়ে সব নকশা চালায় — Bootstrap বা নিজস্ব CSS — সে দেখে তার .btn নিয়ম component-এর ভেতরে পৌঁছায় না। ফন্টের মাপ মেলে না, focus ring অন্যরকম, dark mode-এ একটি component সাদা থেকে যায়।

সমাধান হলো দেয়ালে পরিকল্পিত দরজা রাখা। CSS custom property উত্তরাধিকারে ভেতরে পৌঁছায়, তাই component-নির্মাতা --brand, --radius, --font-এর মতো design token ঘোষণা করে দেন, আর পাতা :root-এ মান বসিয়ে পুরো থিম বদলায় (পর্ব ৩৩)। ::part() দিয়ে নির্মাতা নির্দিষ্ট অংশ — যেমন part="label" — বাইরের CSS-এর জন্য খুলে দেন। উত্তরাধিকারী property (font-family, color) এমনিতেই ঢোকে। এর মানে, দেয়ালটা আসলে একটি চুক্তি: component নিজেই ঘোষণা করে কোনটা বদলানো যাবে, আর বাকিটা তার নিজের দায়িত্ব — ঠিক যেমন একটি ফাংশন তার parameter ঘোষণা করে, ভেতরের ভ্যারিয়েবল লুকিয়ে রাখে।

এই টানাপোড়েনের জন্যই সাম্প্রতিক বছরগুলোতে উল্টো ধারাও বেড়েছে: Shadow DOM ছাড়া, light DOM-এ কাজ করা custom element — যেমন “HTML web components” ধরনের নকশা। তখন পাতার CSS সরাসরি সাজাতে পারে, বিনিময়ে সংঘর্ষের ঝুঁকি ফিরে আসে, যা BEM বা @scope দিয়ে সামলাতে হয়।

মূল উপলব্ধি: Encapsulation সবসময় একটি দর-কষাকষি — বাইরের ভাঙন থেকে সুরক্ষা কিনতে বাইরের নিয়ন্ত্রণ ছাড়তে হয়। ভালো component সেই সীমারেখা স্পষ্ট করে লিখে দেয়: কোন token, কোন part বদলানো যাবে।

প্র ০৩“JavaScript শুধু অবস্থা বদলাবে, চেহারা CSS-এর” — এটি কি নিছক রুচির প্রশ্ন, নাকি এর পেছনে মাপা যায় এমন সুবিধা আছে?

রুচির চেয়ে অনেক বেশি — অন্তত চারটি দিকে সুবিধাটা স্পষ্ট।

  • Cascade-এর সাথে লড়াই নেই। el.style inline style, যা যেকোনো selector-কে হারায়। পরে মোবাইলের জন্য media query, dark mode-এর জন্য prefers-color-scheme, কিংবা উচ্চ-কনট্রাস্ট মোডের জন্য forced-colors — কোনোটাই সেই মানকে বদলাতে পারে না, !important ছাড়া। অবস্থা attribute-এ থাকলে প্রতিটি পরিস্থিতির নিয়ম CSS-এ আলাদা লেখা যায়।
  • একটি সত্যের উৎস। aria-expanded-এ অবস্থা রাখলে CSS, screen reader, আপনার অন্য স্ক্রিপ্ট আর DevTools — সবাই একই জায়গা থেকে পড়ে। অবস্থা আর চেহারা আলাদা জায়গায় থাকলে প্রায়ই দেখা যায় পর্দায় মেনু খোলা, অথচ screen reader বলছে “collapsed” — এমন অমিলের বাগ ধরা কঠিন।
  • কাজের ভাগ। ডিজাইনার বা অন্য ডেভেলপার শুধু CSS বদলে পুরো নকশা পাল্টাতে পারেন, স্ক্রিপ্ট না ছুঁয়ে। পর্ব ৪৬-এর CSS architecture-এর প্রায় সব পদ্ধতিই এই ভাগের ওপর দাঁড়িয়ে।
  • ব্রাউজারের সুবিধা বিনামূল্যে। CSS-এ থাকা পরিবর্তনে transition আর animation নিজে থেকে কাজ করে, prefers-reduced-motion দিয়ে এক জায়গায় বন্ধ করা যায়। JavaScript-এ টাইমার দিয়ে animation বানালে এই সবকিছু নিজেকে লিখতে হয়।

পারফরম্যান্সের দিকটা সূক্ষ্ম। একটি class বদলালেও ব্রাউজারকে style recalculation করতে হয় — কিন্তু অনেকগুলো el.style.x = … একে একে বসিয়ে মাঝে মাঝে offsetHeight-এর মতো মাপ পড়লে ব্রাউজারকে বারবার লেআউট করতে হয় (একে বলে “layout thrashing” সমস্যা)। একটি class-এ সব বদল একসাথে দেওয়া সাধারণত সস্তা আর ভবিষ্যদ্বাণীযোগ্য।

ব্যতিক্রমও আছে: মাউসের অবস্থান, স্লাইডারের মান বা ড্র্যাগের দূরত্বের মতো ক্রমাগত বদলানো সংখ্যা। সেখানে CSS variable-এ কাঁচা সংখ্যা পাঠানোই সবচেয়ে পরিষ্কার পথ — অংশ ৪-এর উদাহরণ।

মূল উপলব্ধি: দায়িত্বের স্পষ্ট ভাগ শুধু কোড সুন্দর রাখে না — সঠিকতা, অ্যাক্সেসিবিলিটি আর রক্ষণাবেক্ষণের খরচ, তিনটিতেই মাপা যায় এমন লাভ দেয়।

প্র ০৪Shadow DOM তো JavaScript দিয়েই চলত — তাহলে declarative shadow DOM বানানোর দরকার পড়ল কেন, আর innerHTML-এ এটি কাজ করে না কেন?

সমস্যাটা ধরা পড়ে সার্ভারে পাতা বানানোর (server-side rendering) সময়। অনেক সাইট — খবরের সাইট, অনলাইন দোকান — চায় পাতা সার্ভারেই পুরো তৈরি হয়ে আসুক, যাতে প্রথম ঝলকেই লেখা দেখা যায় আর সার্চ ইঞ্জিনও পড়তে পারে (পর্ব ১৩, ৪৯)। কিন্তু Shadow DOM-এর ভেতরের কাঠামো HTML-এ লেখার কোনো উপায় ছিল না; সেটি তৈরি হতো কেবল স্ক্রিপ্ট নেমে চলার পর। ফলে component হয় প্রথমে খালি দেখাত, নয়তো অগোছালো অবস্থা থেকে হঠাৎ লাফ দিয়ে সাজানো অবস্থায় যেত — layout shift, খারাপ অভিজ্ঞতা, আর স্ক্রিপ্ট ব্যর্থ হলে কিছুই নয়।

Declarative shadow DOM এই ফাঁক ভরায়: <template shadowrootmode="open"> দেখলেই parser পাতা পড়তে পড়তেই shadow root বানিয়ে ফেলে। স্টাইলসহ পুরো component প্রথম ঝলকেই ঠিক চেহারায় আসে, JavaScript পরে এসে শুধু আচরণ যোগ করে। পথটা সহজ ছিল না — Chrome প্রথমে shadowroot নামে attribute চালু করে, পরে অন্য ব্রাউজারের সাথে আলোচনায় নাম ও কিছু নিয়ম বদলে হয় shadowrootmode; Safari আর ২০২৪ সালের শুরুতে Firefox যোগ দেওয়ার পর এটি Baseline হয়।

innerHTML-এ কেন নয়? নিরাপত্তা আর পুরোনো কোডের কারণে। বহু বছর ধরে অসংখ্য সাইট বাইরের HTML পরিষ্কার (sanitize) করে innerHTML-এ বসায়; সেই sanitizer-গুলো জানত <template> একটি নিরীহ, ঘুমন্ত জিনিস। হঠাৎ innerHTML যদি template থেকে জীবন্ত shadow root বানাতে শুরু করত, পুরোনো sanitizer-কে ফাঁকি দেওয়ার নতুন পথ খুলে যেত। তাই নির্মাতারা আলাদা, নতুন পথ দিলেন — setHTMLUnsafe() আর Document.parseHTMLUnsafe() — নামের ভেতরেই “unsafe” লিখে মনে করিয়ে দেওয়া যে ইনপুট বিশ্বস্ত হতে হবে।

মূল উপলব্ধি: ওয়েবে নতুন ফিচার যোগ করার সময় সবচেয়ে বড় প্রশ্ন প্রায়ই “এটি কী করবে” নয়, বরং “আজকের কোটি পাতার কোন অনুমান এটি ভেঙে দেবে”। পেছনের দিকে সামঞ্জস্য (backward compatibility) ওয়েবের প্রথম আইন।

প্র ০৫Custom element-এর নামে হাইফেন বাধ্যতামূলক, আর নাম-নিবন্ধন পুরো পাতায় একটিই — এই দুই সিদ্ধান্তের পেছনের চিন্তা কী, আর এর দাম কী?

দুটি সিদ্ধান্তই একটি পুরোনো ভয় থেকে এসেছে: নামের সংঘর্ষ। ওয়েবে কোনো কিছু ভাঙা চলে না — একবার কোটি পাতায় একটি নাম ব্যবহার হলে ব্রাউজার আর সেই নামের অর্থ বদলাতে পারে না। এর একটি বিখ্যাত উদাহরণ JavaScript জগৎ থেকে: ২০১৮ সালে array-র জন্য flatten() নামে একটি নতুন method আনার প্রস্তাব ছিল, কিন্তু দেখা গেল MooTools নামের একটি পুরোনো লাইব্রেরি একই নামে নিজের method বসাত, আর নতুন নিয়মে বহু পুরোনো সাইট ভাঙত। শেষে নতুন method-এর নাম রাখতে হয় flat() — ঘটনাটি মজা করে “smooshgate” নামে পরিচিত।

HTML-এর ক্ষেত্রেও একই ঝুঁকি। ধরুন হাজারো সাইট নিজেদের <tabs> বানাল; পরে HTML যদি নিজস্ব <tabs> আনতে চায়, তখন কোনটা কার? হাইফেনের নিয়ম একটি স্থায়ী ভাগাভাগি: হাইফেন ছাড়া নাম HTML-এর, হাইফেনওয়ালা নাম আপনার। তাই HTML-এর নতুন element-গুলোর (যেমন <search>) নামে কখনো হাইফেন থাকে না। বোনাস হিসেবে ব্রাউজার নাম দেখেই বোঝে কোনটি পরে define হতে পারে — অংশ ৬-এর HTMLElement বনাম HTMLUnknownElement.

দাম কোথায়? নাম-নিবন্ধন (customElements) পুরো পাতায় একটিই। একই পাতায় দুটি আলাদা লাইব্রেরি যদি দুটি ভিন্ন <ui-button> আনে, বা একই লাইব্রেরির দুটি সংস্করণ আসে, তাহলে দ্বিতীয়টির define NotSupportedError দেবে। বড় প্রতিষ্ঠানে, যেখানে একই পাতায় অনেক দলের কোড চলে, এটি বাস্তব সমস্যা। প্রচলিত সমাধান হলো নামের আগে প্রতিষ্ঠানের চিহ্ন বসানো (যেমন abcl-button) আর define-এর আগে customElements.get() দিয়ে যাচাই। প্রতিটি অংশের জন্য আলাদা নাম-নিবন্ধন (scoped registry) দেওয়ার একটি প্রস্তাব নিয়েও কাজ চলছে — নতুন ফিচার হিসেবে এর সাপোর্ট Can I use-এ দেখে নেবেন।

মূল উপলব্ধি: একটি বিশ্বব্যাপী প্ল্যাটফর্মে নাম হলো সবচেয়ে দামি সম্পদ। ভালো নকশা আগেই ঠিক করে দেয় কোন নাম কার — আর সেই নিয়মের সুবিধা ও সীমাবদ্ধতা, দুটোই বহু বছর বয়ে বেড়াতে হয়।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — দেখা, ভাবা আর যাচাই করাই আসল কাজ।

  1. ব্রাউজারের লুকানো shadow খুঁজুন: Chrome বা Edge-এর DevTools-এর Settings → Preferences → Elements-এ “Show user agent shadow DOM” চালু করুন। তারপর যেকোনো পাতায় একটি <input type="range"> বা <video controls> Inspect করুন। কী দেখলেন?

    element-এর নিচে #shadow-root (user-agent) লেখা একটি শাখা দেখবেন; খুললে ভেতরে কয়েকটি div — স্লাইডারের দাগ (track) আর হাতল (thumb), কিংবা ভিডিওর প্লে বোতাম, সময়ের দাগ, শব্দের নিয়ন্ত্রণ। অর্থাৎ ব্রাউজার নিজের কন্ট্রোল Shadow DOM দিয়েই বানায়। এজন্যই পাতার সাধারণ CSS সেগুলোর ভেতরে পৌঁছায় না, আর ব্রাউজারকে আলাদা pseudo-element (যেমন ::-webkit-slider-thumb) বা নতুন মানদণ্ড দিতে হয় সাজানোর জন্য (পর্ব ৪৩)।

  2. পুরোনো কোড শোধরান: অংশ ১০-এর AI-এর btn.onclick কোডটি এমনভাবে নতুন করে লিখুন, যাতে JavaScript শুধু aria-expanded বদলায় আর সব চেহারা CSS-এ থাকে। স্ক্রিপ্ট বন্ধ থাকলে কী হবে, সেটিও ভাবুন।

    একটি সম্ভাব্য উত্তর (একটি নোটিশ বাক্সের উদাহরণে) — এখানে hidden attribute অবস্থার অংশ, আর রং-বর্ডার CSS-এ:

    <style>
      .alert { padding: 12px; background: #fef3c7; }
      .alert[hidden] { display: none; }
    </style>
    <button type="button" aria-expanded="false" aria-controls="tip">নোটিশ দেখুন</button>
    <div class="alert" id="tip" hidden>আগামীকাল অফিস বন্ধ থাকবে।</div>
    <script>
      const b2 = document.querySelector('[aria-controls="tip"]');
      const tip = document.querySelector('#tip');
      b2.addEventListener('click', () => {
        const open = b2.getAttribute('aria-expanded') === 'true';
        b2.setAttribute('aria-expanded', String(!open));
        tip.hidden = open;
      });
    </script>

    JavaScript-এ কোনো রং নেই; hidden HTML-এর নিজস্ব attribute, আর বোতামের চেহারা চাইলে [aria-expanded="true"] দিয়ে CSS-এ। স্ক্রিপ্ট বন্ধ থাকলে নোটিশটি কখনো খুলবে না — তাই সত্যিকারের পাতায় হয় <details> ব্যবহার করুন, নয়তো hidden attribute-টি স্ক্রিপ্ট দিয়েই বসান, যাতে স্ক্রিপ্ট না চললে লেখাটি খোলা থাকে।

  3. নাম যাচাই: নিচের কোনগুলো বৈধ custom element নাম, আর কেন? my-card, MyCard, card, x-1, font-face, 1-card, abcl-price-tag.

    বৈধ: my-card, x-1 (ছোট হাতের অক্ষরে শুরু, হাইফেন আছে — সংখ্যা পরে থাকতে পারে), abcl-price-tag (একাধিক হাইফেনও চলে; প্রতিষ্ঠানের চিহ্ন দিয়ে শুরু করা ভালো অভ্যাস)।

    অবৈধ: MyCard (বড় হাতের অক্ষর, হাইফেন নেই), card (হাইফেন নেই), font-face (SVG-র জন্য সংরক্ষিত নাম), 1-card (সংখ্যা দিয়ে শুরু)। অংশ ৬-এর স্ক্রিপ্টে এগুলো একে একে define করে দেখতে পারেন — অবৈধগুলোতে SyntaxError আসবে।

  4. AI অডিট: যেকোনো AI-কে বলুন “একটি tabs web component বানাও।” তারপর অংশ ১০-এর চেকলিস্ট ধরে যাচাই করুন। বিশেষ করে দেখুন: ট্যাবগুলো আসল <button> কি না, অবস্থা কোথায় রাখা হয়েছে, আর স্ক্রিপ্ট বন্ধ থাকলে কী দেখায়।

    সাধারণত যা দেখা যায়:

    • ট্যাবগুলো <div> বা <span> দিয়ে, click handler-সহ — Tab কি দিয়ে পৌঁছানো যায় না, role="tab", aria-selected নেই।
    • প্যানেল দেখানো-লুকানো style.display দিয়ে, অথচ hidden attribute বা aria-selected-ভিত্তিক CSS অনেক পরিষ্কার হতো।
    • সব কাঠামো innerHTML দিয়ে shadow-এর ভেতরে বানানো — স্ক্রিপ্ট বন্ধ থাকলে পাতায় কিছুই নেই। ভালো সমাধান: light DOM-এ আগে থেকেই শিরোনাম আর লেখা রেখে component দিয়ে শুধু ট্যাবে রূপ দেওয়া (অর্থাৎ HTML web component বানানো)।
    • প্রায়ই তীর-কি (← →) দিয়ে ট্যাব বদলের সুবিধা থাকে না, যা ARIA-র tabs নকশায় প্রত্যাশিত।

    এই ভুলগুলো ধরতে পারাটাই আসল দক্ষতা — কোড লেখা AI পারে, কিন্তু কোনটা সবার জন্য কাজ করে, তা বিচার করেন আপনি।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রম<template> থেকে পাতায় একটি সারি বসানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📄 HTML-এ <template id="row">-এর ভেতরে কাঠামো লেখা
  2. 😴 Parser ভেতরটা ঘুমন্ত template.content-এ রাখে — পাতায় দেখা যায় না
  3. 📋 JavaScript content.cloneNode(true) দিয়ে কপি বানায়
  4. ✍️ কপির ভেতরে textContent দিয়ে লেখা ভরা
  5. 📌 append() দিয়ে কপি পাতায় বসানো — এবার দেখা যায়

নিচের কোনটি একটি বৈধ custom element নাম?

  • <UserCard>
  • <usercard>
  • <user-card>
  • <1-card>

পাতার CSS-এ p { color: red } আর my-card { --accent: purple } আছে। my-card-এর Shadow DOM-এর ভেতরের একটি সাধারণ <p> (নিজের কোনো রং নেই) আর var(--accent) ব্যবহার করা বর্ডার কেমন হবে?

  • অনুচ্ছেদ লাল, বর্ডার বেগুনি
  • অনুচ্ছেদ লাল নয়, বর্ডার বেগুনি
  • অনুচ্ছেদ লাল নয়, বর্ডার বেগুনি নয়
  • অনুচ্ছেদ লাল, বর্ডার বেগুনি নয়

একটি <template>-এর ভেতরে <img src="photo.webp"> লেখা আছে। ছবিটি কখন নামানো (লোড) হয়?

  • পাতা লোড হওয়ার সময়ই
  • মাউস template-এর ওপর নিলে
  • cloneNode(true) চালানোর সঙ্গে সঙ্গে, পাতায় না বসালেও
  • কপি করে পাতার DOM-এ বসানোর পর

একটি বোতাম চাপলে মেনু খুলবে। নিচের কোন পদ্ধতিটি সবচেয়ে ভালো?

  • JavaScript বোতামের aria-expanded বদলায়, CSS [aria-expanded="true"] + .menu দিয়ে মেনু দেখায়
  • JavaScript menu.style.display = 'block' আর btn.style.background বসায়
  • একটি <div onclick> দিয়ে মেনু খোলা
  • প্রতিবার innerHTML দিয়ে পুরো মেনু নতুন করে লেখা

Shadow DOM-এর ভেতরের CSS-এ ::slotted(p) লেখা হলো। এটি কোন element-কে সাজায়?

  • পুরো পাতার সব <p>
  • শুধু shadow-এর ভেতরে লেখা <p>
  • host-এর ভেতর থেকে slot-এ সরাসরি বসা <p>
  • slot-এ বসা element-এর ভেতরের সব গভীর <p>

একটি <fancy-button> custom element-এ শুধু click handler লেখা হয়েছে, ভেতরে কোনো native element নেই। প্রধান সমস্যা কী?

  • নামটি অবৈধ, তাই ব্রাউজার element-টি দেখাবে না
  • এর কোনো role নেই, কি-বোর্ডে ফোকাস হয় না, Enter-এ কাজ করে না
  • মাউস দিয়ে চাপলেও click event ঘটে না
  • Shadow DOM না থাকায় element-টি কাজ করে না

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ব্রাউজার দেখায় DOM, ফাইল নয়; JavaScript DOM বদলালে ব্রাউজার CSS আবার মিলিয়ে নতুন করে আঁকে।
  2. CSS চেহারার, JS অবস্থার: classList, data-*, aria-* বদলান; রং-মাপ CSS-এ। ক্রমাগত সংখ্যা হলে CSS variable.
  3. <template> ঘুমন্ত ছাঁচ — cloneNode(true), textContent, append(); বাইরের লেখা কখনো innerHTML-এ নয়।
  4. Custom element-এর নামে হাইফেন; Shadow DOM-এর দেয়াল পেরোয় শুধু উত্তরাধিকারী property, CSS variable আর ::part(); shadowrootmode দিয়ে JavaScript ছাড়াই shadow.
  5. Custom element কোনো বাটন নয় — ভেতরে native element; যা আছে তাকে উন্নত করুন (HTML web components); widget-এ Web Components, ডেটায় ঠাসা অ্যাপে framework.

পরের পর্বে: আপনার সাইট এখন সুন্দর, অ্যাক্সেসিবল আর ইন্টারঅ্যাকটিভ — কিন্তু এখনো শুধু আপনার কম্পিউটারে। পর্ব ৫৩-এ Git দিয়ে কাজের ইতিহাস রাখা, GitHub-এ তোলা, আর GitHub Pages, Netlify বা cPanel দিয়ে সাইটটি সারা বিশ্বের জন্য প্রকাশ করা।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান