এই পর্বে যা শিখবেন
- একটি আসল ক্লায়েন্ট-মুখী সাইটের পরিকল্পনা — লক্ষ্য, পাঠক, content inventory আর সাইটম্যাপ
- token-ভিত্তিক design system: oklch রং, contrast-এর হিসাব, fluid type scale, spacing scale, light/dark থিম
- framework ছাড়া প্রতিটি component — popover মেনু, hero, container-query কার্ড, chip, যাচাই-করা ফর্ম, footer
- মোশন, রেসপন্সিভ ছবি, অ্যাক্সেসিবিলিটি আর পারফরম্যান্সের পেশাদার চেকলিস্ট
- GitHub Pages-এ প্রকাশ, রুব্রিক দিয়ে নিজের কাজ মাপা, আর পরের ধাপের extension
১ · পরিকল্পনা — লক্ষ্য, পাঠক, বিষয়বস্তু আর সাইটম্যাপ
পর্ব ১ থেকে ৫৩ পর্যন্ত আপনি টুকরো টুকরো দক্ষতা জোগাড় করেছেন। এই ক্যাপস্টোনে সব জুড়ে একটি জিনিস বানাবেন, যা সত্যিই কাজে লাগে — আপনার নিজের পোর্টফোলিও সাইট। Fiverr, Upwork বা Facebook গ্রুপে কাজ চাইলে ক্লায়েন্ট প্রথমেই বলেন, “আগের কাজ দেখান।” একটি PDF বা স্ক্রিনশটের চেয়ে নিজের বানানো, দ্রুত আর সুন্দর একটি সাইটই সবচেয়ে বড় প্রমাণ — কারণ সাইটটি নিজেই আপনার দক্ষতার নমুনা।
উদাহরণ হিসেবে আমাদের কাল্পনিক “ক্লায়েন্ট” নাফিসা রহমান — খুলনার একজন বিশ্ববিদ্যালয় শিক্ষার্থী, পাশাপাশি ফ্রিল্যান্স ফ্রন্ট-এন্ড ডেভেলপার। আপনি নিজের নাম, নিজের কাজ বসিয়ে নেবেন। কোড লেখার আগে নিচের তিনটি প্রশ্নের উত্তর লিখে ফেলুন:
লক্ষ্য
নতুন ক্লায়েন্ট যেন ৩০ সেকেন্ডে বোঝেন নাফিসা কী করেন, তিনটি কাজ দেখেন, আর যোগাযোগ করেন।
সফলতার মাপ: মাসে কয়টি বার্তা এলোপাঠক
দেশি ছোট ব্যবসার মালিক (প্রায়ই ফোনে, ধীর নেটে), বিদেশি ক্লায়েন্ট (ল্যাপটপে, ইংরেজিতেও পড়বেন), আর নিয়োগকর্তা।
মোবাইল-ফার্স্ট, হালকা, অ্যাক্সেসিবলসীমাবদ্ধতা
বাজেট শূন্য, ডেটাবেস নেই, নিজেই আপডেট করবেন — তাই দুটি ফাইলের static site, বিনা খরচে প্রকাশ।
শুধু index.html + style.cssএরপর content inventory — সাইটে যা যা থাকবে তার পূর্ণ তালিকা, কোড ছোঁয়ার আগেই। এটা বিয়ের দাওয়াতের তালিকার মতো: আগে লিখে না রাখলে কেউ বাদ পড়ে, কেউ দুবার আসে। নাফিসার তালিকা: নাম আর এক লাইনের পরিচয়, একটি ছবি, ৩–৪টি প্রজেক্ট (প্রতিটির ছবি, শিরোনাম, দুই লাইনের বিবরণ, ব্যবহৃত প্রযুক্তি), নিজের কথা, দক্ষতার তালিকা, যোগাযোগের ফর্ম, ইমেইল, GitHub আর LinkedIn-এর লিংক। লেখা আসল হতে হবে — “Lorem ipsum” দিয়ে নকশা করলে পরে আসল বাংলা লেখা বসালে লাইন ভাঙা, উচ্চতা সব বদলে যায় (পর্ব ১৪-এ দেখেছেন, বাংলায় অক্ষরের ওপরে-নিচে চিহ্ন বেশি, তাই লাইনের উচ্চতাও বেশি লাগে)।
| অংশ | ঠিকানা | পাঠকের যে প্রশ্নের উত্তর | যে পর্বগুলো কাজে লাগবে |
|---|---|---|---|
| হোম (hero) | ./ | ইনি কে, আমার কাজে লাগবেন কি? | ৪, ২১, ৩৫ |
| কাজ | #projects | আগে কী বানিয়েছেন? | ২৮, ৩১, ৩২ |
| আমার কথা | #about | বিশ্বাস করা যায়? কী কী পারেন? | ৬, ৭, ৪৪ |
| যোগাযোগ | #contact | কীভাবে কাজের কথা বলব? | ৮, ৯, ৪৩ |
পর্ব ১৫-এর পাঠাগার-সাইট ছিল চার আলাদা পাতা; এখানে প্রথম সংস্করণ এক পাতায় চার অংশ — পোর্টফোলিওতে পাঠক সাধারণত ওপর থেকে নিচে স্ক্রল করেই সব দেখেন। প্রজেক্ট বাড়লে প্রতিটির জন্য আলাদা “case study” পাতা (projects/grocery.html) হবে — সেটি অনুশীলনের extension. শেষে কাগজে একটি wireframe আঁকুন: ফোনে এক কলাম, ল্যাপটপে কোথায় কী পাশাপাশি। পুরো কাজের ধারা ধাপে ধাপে দেখুন:
- পরিকল্পনা
লক্ষ্য·পাঠককাগজে - Token
:rootরং·মাপ - কঙ্কাল
mainlandmark - Component
.cardএকটি একটি করে - পলিশ
transitionমোশন·ছবি - যাচাই
Lighthouseচেকলিস্ট - প্রকাশ
git pushলিংক শেয়ার
- পরিকল্পনা। লক্ষ্য, পাঠক, content inventory, সাইটম্যাপ আর wireframe — সব কাগজে। এখানে এক ঘণ্টা বাঁচাতে গেলে পরে দশ ঘণ্টা যায়, কারণ “কোথায় কী বসবে” প্রশ্নটি কোড লিখতে লিখতে বারবার ফিরে আসে।
- Design token. রং, লেখার মাপ, ফাঁক আর কোণের সিদ্ধান্ত একবারে নিয়ে
:root-এ নাম দিয়ে রাখা (অংশ ২)। এরপর কোনো component-এ “কোন সবুজটা?” ভাবতে হয় না। - HTML কঙ্কাল। doctype, head, skip link, header, main, চারটি section, footer — CSS ছাড়াই যেন পাতা পড়া যায় (অংশ ৩)।
- Component, একটি একটি করে। header-মেনু, hero, কার্ড, about, ফর্ম, footer — প্রতিটি আলাদা ছোট টুকরো, প্রতিটি ফোনে আর বড় পর্দায় পরীক্ষা করে তবে পরেরটিতে যাওয়া (অংশ ৪–৬)।
- পলিশ। হালকা মোশন (কম-নড়াচড়া চাওয়া পাঠককে সম্মান করে), রেসপন্সিভ ছবি, ফোকাসের দাগ (অংশ ৭)।
- যাচাই। validator, কি-বোর্ড, screen reader, Lighthouse, ফোন — চেকলিস্ট আর রুব্রিক ধরে (অংশ ৮–৯)। “আমার ল্যাপটপে ঠিক দেখাচ্ছে” যাচাই নয়।
- প্রকাশ আর শেয়ার। Git দিয়ে GitHub-এ, সেখান থেকে GitHub Pages (পর্ব ৫৩)। এরপর লিংকটি CV, LinkedIn আর ক্লায়েন্টকে পাঠানো বার্তায় — আর প্রতিটি নতুন কাজের পর সাইটে যোগ।
২ · আগে design system — রং, লেখা, ফাঁক আর কোণের token
বাড়ি রং করার আগে মিস্ত্রি পুরো বাড়ির জন্য তিন-চারটি রং ঠিক করে নেন; প্রতিটি ঘরে নতুন করে দোকানে যান না। Design token ঠিক এই সিদ্ধান্তগুলোর নাম — --brand, --space-m, --step-3. পর্ব ৩৩-এ দেখেছেন custom property কীভাবে কাজ করে; পর্ব ৪৬-এ দেখেছেন বড় প্রজেক্টে নামকরণ কেন জরুরি। এখানে সব একসাথে, চারটি পরিবারে:
- রং —
oklch()-তে। পর্ব ২০-এ দেখেছেন, oklch-এর প্রথম সংখ্যাটি (L) মানুষের চোখে উজ্জ্বলতার কাছাকাছি (perceptual uniformity). তাই “লেখা 25%, পটভূমি 98.5%” — এই ফাঁক দেখেই contrast আন্দাজ করা যায়, আর hue (শেষ সংখ্যা) বদলে পুরো থিমের রং বদলানো যায় উজ্জ্বলতা না হারিয়ে।oklch()এখন Baseline Widely available. - লেখার মাপ — type scale,
clamp()দিয়ে।--step-0সাধারণ লেখা,--step-3h1. প্রতিটি clamp(সর্বনিম্ন, পছন্দের, সর্বোচ্চ) — ফোনে ছোট, বড় পর্দায় বড়, মাঝে মসৃণভাবে (fluid typography, পর্ব ৩৫)। মাঝের মানেremরাখুন, শুধুvwনয় — নাহলে ব্রাউজারে লেখা বড় করলে (zoom) আকার বাড়ে না, যা WCAG-এর লেখা বড় করার শর্ত ভাঙে। - ফাঁক — spacing scale. ইচ্ছেমতো 13px, 17px না লিখে কয়েকটি ধাপ:
2xs0.25rem থেকেxlপর্যন্ত। পাতায় একটা ছন্দ আসে, চোখ আরাম পায়। - কোণ — radius. ছোট (বাটন, ইনপুট), মাঝারি (কার্ড), আর chip-এর জন্য পুরো গোল।
নিচের এডিটরে নাফিসার token-এর ছোট একটি রূপ। ওপরের “থিম” বাছাই করে দেখুন — এক লাইনও JavaScript ছাড়া পুরো পাতা অন্ধকার হয়:
:root {
color-scheme: light dark;
--bg: light-dark(oklch(98.5% 0.006 250), oklch(18% 0.02 260));
--text: light-dark(oklch(25% 0.03 260), oklch(95% 0.01 260));
--muted: light-dark(oklch(46% 0.03 260), oklch(76% 0.02 260));
--brand: light-dark(oklch(50% 0.11 160), oklch(78% 0.13 165));
--on-brand: light-dark(oklch(100% 0 0), oklch(20% 0.04 165));
--step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--step-3: clamp(1.75rem, 1.2rem + 3vw, 3.25rem);
--space-s: 0.75rem;
--space-m: 1.25rem;
--radius-m: 0.75rem;
}
:root:has(#theme option[value="light"]:checked) { color-scheme: light; }
:root:has(#theme option[value="dark"]:checked) { color-scheme: dark; }
body { margin: 0; background: var(--bg); color: var(--text);
font: var(--step-0)/1.6 system-ui, sans-serif; }
.page { padding: var(--space-m); }
h1 { font-size: var(--step-3); line-height: 1.15; margin: var(--space-m) 0 0; }
.muted { color: var(--muted); margin-top: 0; }
.btn { display: inline-block; padding: var(--space-s) var(--space-m);
background: var(--brand); color: var(--on-brand); font-weight: 600;
border-radius: var(--radius-m); text-decoration: none; }প্রিভিউতে হালকা পটভূমিতে নাম, ধূসর পরিচয়-লাইন আর একটি সবুজ বাটন। “অন্ধকার” বাছলে পটভূমি গাঢ় নীলচে, লেখা প্রায় সাদা, বাটন উজ্জ্বল সবুজ আর তার লেখা গাঢ় — কারণ প্রতিটি token-এ light-dark(হালকার রং, অন্ধকারের রং) লেখা, আর color-scheme বলে দিচ্ছে কোনটি চলবে। :root:has(…:checked) হলো পর্ব ৩৪-এর :has() — “যে পাতায় ‘অন্ধকার’ বাছা আছে”। চেষ্টা করুন: --brand-এর দুই জায়গায় hue 160/165 বদলে 250 করুন — নীল থিম, উজ্জ্বলতা আর contrast প্রায় একই থাকে। তারপর হালকার --muted-এর L 46% থেকে 70% করুন — লেখা ঝাপসা হয়ে পড়া কঠিন হয়ে যায়, ঠিক এই ভুলটিই contrast-যাচাই ধরে।
Contrast-এর হিসাব আন্দাজে নয়। WCAG 2.2-এ সাধারণ লেখার জন্য contrast ratio অন্তত ৪.৫:১, বড় লেখা আর বাটনের সীমারেখার মতো UI অংশের জন্য ৩:১ (পর্ব ৪৫)। নাফিসার token-গুলো sRGB-তে রূপান্তর করে WCAG-এর সূত্রে মেপে পাওয়া মান:
| জোড়া | হালকা থিম | অন্ধকার থিম | শর্ত |
|---|---|---|---|
--text / --bg | ১৫.৩:১ | ১৬.৩:১ | ৪.৫:১ ✔ |
--muted / --bg | ৬.৮:১ | ৮.৮:১ | ৪.৫:১ ✔ |
--brand / --bg (লিংক) | ৫.৪:১ | ৯.৯:১ | ৪.৫:১ ✔ |
--on-brand / --brand (বাটন) | ৫.৭:১ | ৯.৫:১ | ৪.৫:১ ✔ |
--line / --surface | ১.৪:১ | ১.৫:১ | শুধু সাজ — ইনপুটের সীমায় নয় |
শেষ সারিটি খেয়াল করুন: হালকা --line কার্ডের চারপাশের সাজের জন্য ঠিক আছে, কিন্তু ইনপুট বক্সের সীমা চেনার একমাত্র উপায় হলে ৩:১ লাগবে — তাই পুরো কোডে ইনপুটের সীমায় --muted (৭:১-এর মতো) ব্যবহার হয়েছে। নিজের রং বদলালে DevTools-এর color picker-এ contrast লেখা থাকে (পর্ব ৪৭), সেটি দেখে নিন।
আরও গভীরে: সাধারণ (register না করা) custom property ব্রাউজার শুধু লেখা হিসেবে মনে রাখে; var(--bg) যে element-এ ব্যবহার হয়, মানটি সেখানে বসে, আর light-dark() হিসাব হয় সেই element-এর color-scheme দেখে। তাই .promo { color-scheme: dark; } লিখলে শুধু ওই অংশটুকু অন্ধকার থিম পায় — token নতুন করে লিখতে হয় না। light-dark() ২০২৪ সালে Baseline Newly available হয়েছে; এর চেয়ে পুরোনো ব্রাউজারের জন্য পুরো কোডে @supports not (color: light-dark(#fff, #000)) দিয়ে হালকা থিমের hex মান রাখা আছে (@supports, পর্ব ৪৮)। custom property-র ভেতরে “আগে পুরোনো মান, পরে নতুন” লেখার কৌশল কাজ করে না, কারণ custom property যেকোনো লেখাই বৈধ ধরে নেয়।
৩ · HTML কঙ্কাল — landmark, skip link আর head
বাড়ির নকশায় আগে কলাম আর বিম, তারপর টাইলস। পর্ব ১০-এ শিখেছেন semantic layout, পর্ব ১২-এ landmark, পর্ব ১৩-এ head আর meta. এখানে সব এক ফাইলে। CSS প্রায় নেই — ইচ্ছাকৃতভাবে, যাতে দেখেন CSS ছাড়াও পাতাটি পড়া যায়:
<!doctype html>
<html lang="bn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>নাফিসা রহমান — ফ্রিল্যান্স ফ্রন্ট-এন্ড ডেভেলপার, খুলনা</title>
<meta name="description" content="ছোট ব্যবসার জন্য দ্রুত ও অ্যাক্সেসিবল ওয়েবসাইট। কাজের নমুনা দেখুন, যোগাযোগ করুন।">
<style>
.skip-link { position: absolute; left: 0.5rem; top: -3rem; }
.skip-link:focus { top: 0.5rem; }
</style>
</head>
<body>
<a class="skip-link" href="#main">সরাসরি মূল অংশে যান</a>
<header>
<a href="#top">নাফিসা.dev</a>
<nav aria-label="প্রধান মেনু"><a href="#projects">কাজ</a> · <a href="#about">আমার কথা</a> · <a href="#contact">যোগাযোগ</a></nav>
</header>
<main id="main" tabindex="-1">
<section aria-labelledby="hero-title"><h1 id="hero-title">ছোট ব্যবসার জন্য দ্রুত ওয়েবসাইট</h1></section>
<section id="projects" aria-labelledby="projects-title"><h2 id="projects-title">আমার কাজ</h2></section>
<section id="about" aria-labelledby="about-title"><h2 id="about-title">আমার কথা</h2></section>
<section id="contact" aria-labelledby="contact-title"><h2 id="contact-title">যোগাযোগ</h2></section>
</main>
<footer><p><small>© ২০২৬ নাফিসা রহমান</small></p></footer>
</body>
</html>প্রিভিউতে ব্রাউজারের সাধারণ চেহারায় লোগো-লিংক, মেনু, একটি h1, তিনটি h2 আর footer. “সরাসরি মূল অংশে যান” লেখাটি দেখা যাচ্ছে না — প্রিভিউর ভেতরে একবার ক্লিক করে Tab চাপুন, ওপরে সেটি ভেসে উঠবে। এটি skip link: কি-বোর্ড ব্যবহারকারী প্রতিটি পাতায় মেনুর সব লিংক পার না হয়ে এক চাপে মূল অংশে যান। চেষ্টা করুন: <main>-কে <div> করে দিন — চেহারা একটুও বদলাবে না, কিন্তু screen reader-এর landmark তালিকা থেকে “main” হারিয়ে যাবে। চোখে না পড়া ক্ষতিই সবচেয়ে বড় ক্ষতি।
কয়েকটি সিদ্ধান্তের কারণ: প্রতিটি section-এ aria-labelledby তার h2-কে নাম হিসেবে দেয়, ফলে section-টি নামসহ “region” landmark হয়। পাতায় h1 একটিই — hero-র বড় বাক্যটি, নাম নয়; নাম তো <title> আর লোগোতে আছেই। আর <title>-এ নাম, পেশা আর শহর — Google-এ “ফ্রন্ট-এন্ড ডেভেলপার খুলনা” খুঁজলে এটুকুই প্রথমে চোখে পড়ে। পুরো head-এ (অংশ ৯) আরও থাকবে canonical, Open Graph (WhatsApp বা Facebook-এ লিংক পাঠালে যে কার্ড দেখায়), দুই থিমের theme-color আর favicon.
৪ · Header আর মোবাইল মেনু — popover দিয়ে, framework ছাড়া
বড় পর্দায় মেনুর তিনটি লিংক পাশাপাশি জায়গা পায়; ফোনে জায়গা নেই, তাই একটি “মেনু” বাটন। কয়েক বছর আগেও এর জন্য JavaScript লাগত — ক্লিক ধরা, খোলা-বন্ধের অবস্থা রাখা, Esc চাপলে বন্ধ করা, বাইরে চাপলে বন্ধ করা, ফোকাস সামলানো। পর্ব ১১-এ দেখেছেন, HTML-এর popover attribute এই সবই নিজে করে: বাটনে popovertarget="menu", মেনুতে popover, ব্যস।
<style>
body { font-family: system-ui, sans-serif; margin: 0.5rem; }
.frame { width: 22rem; min-height: 17rem; resize: horizontal; overflow: auto; border: 2px dashed #999; }
.site-header { container-type: inline-size; border-bottom: 1px solid #d4d8de; }
.bar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.75rem 1rem; }
.logo { font-weight: 700; color: inherit; text-decoration: none; }
.menu-btn { font: inherit; padding: 0.5rem 0.9rem; border-radius: 0.5rem; }
#menu:popover-open { inset: 4rem auto auto 1rem; margin: 0; padding: 0.75rem; min-width: 12rem;
border: 1px solid #d4d8de; border-radius: 0.75rem; }
#menu ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.25rem; }
#menu a { display: block; padding: 0.5rem 0.75rem; color: inherit; }
@container (width > 36rem) {
.menu-btn { display: none; }
#menu { display: block; position: static; margin: 0; padding: 0; border: 0; background: none; }
#menu ul { display: flex; }
}
</style>
<div class="frame">
<header class="site-header">
<div class="bar">
<a class="logo" href="#top">নাফিসা.dev</a>
<button class="menu-btn" type="button" popovertarget="menu">মেনু</button>
<nav id="menu" popover aria-label="প্রধান মেনু">
<ul>
<li><a href="#projects">কাজ</a></li>
<li><a href="#about">আমার কথা</a></li>
<li><a href="#contact">যোগাযোগ</a></li>
</ul>
</nav>
</div>
</header>
<p style="padding: 0 1rem">পাতার বাকি অংশ…</p>
</div>ড্যাশ দেওয়া বাক্সটি একটি “ফোনের পর্দা” (22rem চওড়া)। “মেনু” চাপুন — লিংকগুলো একটি ভাসমান বাক্সে খোলে (প্রিভিউতে বাঁ দিকে; আসল ফোনে পর্দাই ফ্রেম, তাই পুরো কোডে বাক্সটি ডানে)। বাইরে কোথাও চাপুন বা Esc চাপুন, বন্ধ হয়ে যায় (light dismiss). খোলা মেনু থাকে top layer-এ, তাই অন্য কোনো element-এর z-index তাকে ঢাকতে পারে না। এবার .frame-এর width: 22rem বদলে 100% করুন (বা ডেস্কটপে বাক্সের ডান-নিচের কোণা টেনে বড় করুন): header 36rem পেরোলেই বাটন লুকায়, লিংকগুলো এক সারিতে চলে আসে।
এখানে তিনটি কৌশল একসাথে কাজ করছে। প্রথমত, খোলা অবস্থার জায়গা (inset, margin) লেখা হয়েছে #menu:popover-open-এ, সাধারণ #menu-এ নয়। ব্রাউজার popover না চিনলে এই selector-ও চেনে না, তাই নিয়মটি বাদ পড়ে আর মেনুটি সাধারণ তালিকা হয়ে পাতায় দেখা যায় — কাজ চলে, শুধু সাজ কম। এটাই progressive enhancement; পুরো কোডে @supports not selector(:popover-open) দিয়ে তখন অকেজো বাটনটিও লুকানো হয়েছে। দ্বিতীয়ত, বড় পর্দায় আমরা popover-কে সাধারণ element বানিয়ে ফেলি: ব্রাউজারের নিজের CSS বন্ধ মেনুকে display: none দেয়, আর cascade-এর origin নিয়মে আমাদের সাধারণ নিয়ম ব্রাউজারের নিজের CSS-এর ওপরে জেতে (পর্ব ১৮), তাই display: block; position: static; margin: 0 দিয়েই কাজ শেষ। তৃতীয়ত, ডেমোতে ঘর ছোট-বড় দেখাতে container query, কিন্তু আসল সাইটে পাতার মেনু পুরো পর্দার ব্যাপার, তাই পুরো কোডে @media (width >= 48rem).
Popover এখন সব প্রধান ব্রাউজারে আছে (Baseline Newly available). দুটি সীমা জেনে রাখুন। মেনুর ভেতরের #projects লিংকে চাপলে পাতা ঠিকই সেখানে যায়, কিন্তু মেনু নিজে বন্ধ হয় না — বাইরে চাপতে হয়; চাইলে এক লাইনের JavaScript দিয়ে বন্ধ করা যায়। আর একেবারে পুরোনো ব্রাউজারও সমান গুরুত্ব পেলে <details><summary>মেনু</summary>…</details> (disclosure widget) আরও পুরোনো আর নিরাপদ পথ — তবে সেখানে Esc বা বাইরে চাপলে বন্ধ হয় না। “div-এ onclick দিয়ে হ্যামবার্গার” — এই পুরোনো পথ নয়; সেটি কি-বোর্ডে প্রায়ই চলে না।
৫ · Hero আর প্রজেক্ট কার্ডের grid — auto-fit আর container query
Hero হলো দোকানের সাইনবোর্ড — পাতা খুলেই প্রথম পর্দা। তিনটি কাজ: কে, কী করেন, এখন কী করবেন (বাটন)। বড় লেখা হবে ছোট ফোনে ঠিকঠাক, বড় পর্দায় জমকালো; আর লাইন ভাঙবে সুন্দরভাবে।
body { margin: 0; font-family: system-ui, sans-serif; }
.hero {
--brand: oklch(50% 0.11 160);
padding: clamp(2rem, 6vw, 5rem) 1.25rem;
background:
radial-gradient(circle at 85% 15%, oklch(85% 0.08 160 / 0.6), transparent 45%),
oklch(98.5% 0.006 250);
}
.eyebrow { color: var(--brand); font-weight: 600; margin: 0; }
h1 {
font-size: clamp(1.75rem, 1.2rem + 3vw, 3.25rem);
line-height: 1.15;
max-inline-size: 22ch;
text-wrap: balance;
margin-block: 0.5rem 1rem;
}
.lead { max-inline-size: 60ch; font-size: 1.125rem; line-height: 1.7; color: #4e5969; }
.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.btn { padding: 0.75rem 1.25rem; border-radius: 0.75rem; font-weight: 600;
background: var(--brand); color: #fff; text-decoration: none; }
.btn.ghost { background: transparent; color: var(--brand);
box-shadow: inset 0 0 0 2px var(--brand); }
.btn:focus-visible { outline: 3px solid var(--brand); outline-offset: 3px; }প্রিভিউতে হালকা পটভূমির ডান কোণে সবুজাভ একটি আলোর ছোপ (radial-gradient, পর্ব ২২), সবুজ ছোট লাইন, বড় শিরোনাম আর দুটি বাটন। শিরোনামের লাইনগুলো প্রায় সমান দৈর্ঘ্যের — text-wrap: balance শেষ লাইনে একটি শব্দ একা ঝুলে থাকতে দেয় না (২০২৪ থেকে Baseline Newly available; না চিনলে সাধারণভাবে লাইন ভাঙে, ক্ষতি নেই)। চেষ্টা করুন: text-wrap: balance মুছে দেখুন শেষ লাইন কেমন হয়। তারপর max-inline-size: 22ch বদলে 40ch করুন — শিরোনাম কম লাইনে আসে, কিন্তু বড় অক্ষরের লম্বা লাইনে চোখকে অনেকটা পথ যেতে হয়; কোনটি পড়তে আরাম, তুলনা করুন।
এবার মূল আকর্ষণ — কাজের কার্ড। দুটি আলাদা প্রশ্ন আছে, আর দুটির উত্তর দুটি আলাদা টুল দেয়। “কয়টি কলাম?” — এর উত্তর grid নিজেই হিসাব করে, পর্ব ২৮-এর repeat(auto-fit, minmax()) দিয়ে, কোনো media query ছাড়াই। “একটি কার্ডের ভেতরে ছবি ওপরে না পাশে?” — এর উত্তর কার্ডের নিজের চওড়ার ওপর নির্ভর করে, পর্দার নয়; তাই container query (পর্ব ৩১)।
<style>
body { font-family: system-ui, sans-serif; margin: 0.5rem; }
.frame { resize: horizontal; overflow: auto; border: 2px dashed #999; padding: 0.5rem; }
.grid { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); }
.grid > li { container-type: inline-size; }
.card { display: grid; gap: 0.75rem; height: 100%; box-sizing: border-box;
padding: 0.75rem; border: 1px solid #d4d8de; border-radius: 0.75rem; }
.thumb { aspect-ratio: 16 / 9; border-radius: 0.5rem;
background: linear-gradient(135deg, oklch(80% 0.1 160), oklch(55% 0.12 250)); }
.card h3 { margin: 0 0 0.25rem; font-size: 1.1rem; }
.card p { margin: 0; color: #4e5969; }
@container (width > 24rem) {
.card { grid-template-columns: 9rem 1fr; align-items: start; }
}
</style>
<div class="frame">
<ul class="grid">
<li><article class="card"><div class="thumb"></div><div><h3>মুদি দোকানের অর্ডার পাতা</h3><p>এক হাতে ফোনে অর্ডার, বড় বাটন।</p></div></article></li>
<li><article class="card"><div class="thumb"></div><div><h3>কোচিং সেন্টারের ফলাফল</h3><p>অ্যাক্সেসিবল টেবিল, ভর্তির ফর্ম।</p></div></article></li>
<li><article class="card"><div class="thumb"></div><div><h3>শাপলা গণপাঠাগার</h3><p>চার পাতার সাইট, সময়সূচি আর ফি।</p></div></article></li>
<li><article class="card"><div class="thumb"></div><div><h3>রেস্তোরাঁর মেনু</h3><p>ডার্ক মোড আর দ্রুত লোড।</p></div></article></li>
</ul>
</div>বড় পর্দায় প্রিভিউ ৩টি কলাম দেখায় (ট্যাবলেটে ২টি), চতুর্থ কার্ড পরের সারিতে; প্রতিটি কার্ডে ছবি ওপরে, লেখা নিচে। ডেস্কটপে ড্যাশ দেওয়া বাক্সের ডান-নিচের কোণা টেনে সরু করুন: এক কলামে নামলেই কার্ডগুলো হঠাৎ চওড়া হয়, আর ছবি চলে যায় লেখার বাঁ পাশে। আরও সরু করলে আবার ওপরে-নিচে। ফোনে টানা না গেলে .frame-এ width: 28rem যোগ করে দেখুন। তারপর minmax(min(100%, 15rem), 1fr)-এর 15rem বদলে 10rem করুন — কলাম বাড়বে।
এই বিপরীতমুখী আচরণটাই container query-র জাদু — ব্রাউজার কী হিসাব করছে, চার রকম চওড়ায় ধাপে ধাপে দেখুন:
কলাম = ⌊(340 + 16) ÷ (240 + 16)⌋ = ⌊1.39⌋ = 1 কার্ড = 340px → 24rem (384px)-এর কম → ছবি ওপরে ┌────────────────────┐ │ ▓▓▓▓▓▓ ছবি ▓▓▓▓▓▓ │ │ শিরোনাম · বিবরণ │ └────────────────────┘
কলাম = ⌊(450 + 16) ÷ 256⌋ = ⌊1.82⌋ = 1 কার্ড = 450px → 384px-এর বেশি → ছবি বাঁয়ে ┌──────────────────────────────┐ │ ▓▓ ছবি ▓▓ │ শিরোনাম · বিবরণ │ └──────────────────────────────┘
কলাম = ⌊(700 + 16) ÷ 256⌋ = ⌊2.80⌋ = 2 কার্ড = (700 − 16) ÷ 2 = 342px → আবার ছবি ওপরে ┌─────────────┐ ┌─────────────┐ │ ▓▓▓ ছবি ▓▓▓ │ │ ▓▓▓ ছবি ▓▓▓ │ │ শিরোনাম │ │ শিরোনাম │ └─────────────┘ └─────────────┘
কলাম = ⌊(1100 + 16) ÷ 256⌋ = ⌊4.36⌋ = 4 কার্ড = (1100 − 3 × 16) ÷ 4 = 263px → ছবি ওপরে ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ ছবি │ │ ছবি │ │ ছবি │ │ ছবি │ └──────┘ └──────┘ └──────┘ └──────┘ কার্ড ৪টিই — auto-fit খালি কলাম গুটিয়ে দেয়
- 340px — ছোট ফোন। grid দেখে, কত কলাম বসানো যায় যাতে প্রতিটি অন্তত 15rem (240px) হয় আর মাঝে 16px ফাঁক থাকে। মাত্র ১টি। কার্ডটি 340px — container query-র 24rem-এর কম, তাই ছবি ওপরে।
- 450px — এখনো ১ কলাম, কিন্তু কার্ড চওড়া। দ্বিতীয় কলামের জায়গা হয়নি (দরকার 496px). কার্ডটি একা পুরো 450px পেল — 384px পেরিয়ে গেছে, তাই container query ছবিকে বাঁয়ে বসাল। ছোট পর্দায় “পাশাপাশি” লেআউট — media query দিয়ে এটা বলা যেত না।
- 700px — ২ কলাম, কার্ড আবার সরু। পর্দা বড় হলো, অথচ প্রতিটি কার্ড ছোট হলো (342px), তাই ছবি আবার ওপরে। কার্ড নিজের মাপ দেখে সিদ্ধান্ত নেয় — পর্দার মাপ নয়। এজন্যই একই কার্ড পাতার সরু sidebar-এও ঠিক থাকবে।
- 1100px — ৪ কলাম। ৪.৩৬ থেকে ৪টি পূর্ণ কলাম।
auto-fillহলে বেশি জায়গায় খালি কলাম রেখে দিত;auto-fitখালিগুলো গুটিয়ে আসল কার্ডগুলোকে ছড়িয়ে দেয়। আরmin(100%, 15rem)নিশ্চিত করে, 240px-এর চেয়েও সরু পর্দায় কার্ড বাইরে উপচে পড়বে না।
Container size query এখন Baseline Widely available. মনে রাখুন, container-type যে element-এ দেন, সেটি নিজে query দিয়ে বদলায় না — বদলায় তার ভেতরের element. তাই container হলো <li>, আর বদলানো হচ্ছে ভেতরের .card. পুরো কোডে কার্ডের শিরোনামের লিংকে একটি ::after পুরো কার্ড ঢেকে দেয় (stretched link) — কার্ডের যেকোনো জায়গায় চাপলে প্রজেক্টে যাওয়া যায়, অথচ screen reader শোনে শুধু শিরোনামটুকু, পুরো কার্ডের সব লেখা লিংকের নাম হয়ে যায় না।
৬ · About, দক্ষতার chip আর যোগাযোগ ফর্ম
“আমার কথা” অংশে ক্লায়েন্ট দেখেন মানুষটিকে। একটি ছবি আর তার ছোট বিবরণ — এটাই <figure> আর <figcaption>-এর কাজ (পর্ব ৬)। দক্ষতাগুলো আসলে একটি তালিকা, তাই <ul> — দেখতে ছোট গোল “chip”, কিন্তু screen reader বলে “তালিকা, ৬টি আইটেম”।
<style>
body { font-family: system-ui, sans-serif; margin: 1rem; }
.about { display: grid; gap: 1.5rem; align-items: center;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); }
figure { margin: 0; }
.avatar { display: block; width: 100%; max-width: 13rem; height: auto; border-radius: 50%; }
figcaption { margin-top: 0.5rem; font-size: 0.875rem; color: #4e5969; }
.about h2 { margin-top: 0; }
.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; list-style: none; padding: 0; }
.chips li { padding: 0.3rem 0.8rem; border-radius: 999px; font-size: 0.9rem;
background: oklch(94% 0.03 160); color: oklch(35% 0.08 160); }
</style>
<section class="about" aria-labelledby="about-title">
<figure>
<svg class="avatar" viewBox="0 0 100 100" role="img" aria-label="নাফিসার আঁকা প্রতিকৃতি">
<circle cx="50" cy="50" r="50" fill="#bfe3cf"/>
<circle cx="50" cy="40" r="17" fill="#3b4656"/>
<path d="M18 92 Q50 52 82 92 A50 50 0 0 1 18 92 Z" fill="#3b4656"/>
</svg>
<figcaption>খুলনায় আমার কাজের টেবিলে</figcaption>
</figure>
<div>
<h2 id="about-title">আমার কথা</h2>
<p>আমি নাফিসা। তিন বছর ধরে দোকান, কোচিং সেন্টার আর সংগঠনের জন্য ওয়েবসাইট বানাচ্ছি।</p>
<h3>যেসব কাজে দক্ষ</h3>
<ul class="chips">
<li>Semantic HTML</li><li>রেসপন্সিভ CSS</li><li>অ্যাক্সেসিবিলিটি</li>
<li>বাংলা টাইপোগ্রাফি</li><li>পারফরম্যান্স</li><li>Git ও GitHub</li>
</ul>
</div>
</section>বাঁয়ে গোল একটি আঁকা প্রতিকৃতি (inline SVG, পর্ব ৪৪), নিচে ছোট ধূসর ক্যাপশন; ডানে লেখা আর হালকা সবুজ chip-এর সারি, জায়গা না পেলে পরের লাইনে নামে (flex-wrap). SVG-তে role="img" আর aria-label দিয়ে ছবিটির নাম দেওয়া হয়েছে, ঠিক যেমন <img>-এ alt. চেষ্টা করুন: minmax(min(100%, 14rem), 1fr)-কে 20rem করুন — ছবি আর লেখা ওপর-নিচে চলে আসবে। তারপর একটি নতুন <li> chip যোগ করুন।
আসল সাইটে প্রতিকৃতির জায়গায় আপনার নিজের ছবি — ভালো আলোয়, মুখ স্পষ্ট। ছবির alt-এ “ছবি” শব্দটি লিখবেন না (screen reader নিজেই বলে “image”); লিখুন ছবিতে কী আছে।
এবার যোগাযোগ ফর্ম — পোর্টফোলিওর সবচেয়ে দামি অংশ, কারণ এখানেই কাজ আসে। Static site-এ নিজের সার্ভার নেই, তাই ফর্মের action-এ Formspree-র মতো কোনো ফর্ম-সেবার ঠিকানা বসে (GitHub Pages ফর্ম গ্রহণ করে না; Netlify-র নিজের ফর্ম-সুবিধা আছে)। আর পর্ব ৯ আর ৪৩-এর শিক্ষা: ভুল দেখাতে হবে পাঠক ঘরটি ছুঁয়ে যাওয়ার পরে, পাতা খোলামাত্র নয়।
<style>
body { font-family: system-ui, sans-serif; margin: 1rem; }
form { display: grid; gap: 1rem; max-width: 30rem; }
.field { display: grid; gap: 0.3rem; }
label { font-weight: 600; }
input, textarea { font: inherit; padding: 0.6rem 0.75rem;
border: 1px solid #4e5969; border-radius: 0.5rem; }
:is(input, textarea):focus-visible { outline: 3px solid oklch(50% 0.11 160); outline-offset: 2px; }
.error { display: none; margin: 0; color: oklch(52% 0.19 25); }
.field:has(:user-invalid) :is(input, textarea) { border: 2px solid oklch(52% 0.19 25); }
.field:has(:user-invalid) .error { display: block; }
button { font: inherit; font-weight: 600; padding: 0.75rem; border: 0; border-radius: 0.5rem;
background: oklch(50% 0.11 160); color: #fff; }
</style>
<form action="https://formspree.io/f/YOUR-FORM-ID" method="post">
<div class="field">
<label for="name">আপনার নাম</label>
<input id="name" name="name" required autocomplete="name">
<p class="error">নামটি লিখুন।</p>
</div>
<div class="field">
<label for="email">ইমেইল</label>
<input id="email" name="email" type="email" required autocomplete="email">
<p class="error">একটি সঠিক ইমেইল লিখুন, যেমন name@example.com</p>
</div>
<div class="field">
<label for="message">কাজের বিবরণ (অন্তত ২০ অক্ষর)</label>
<textarea id="message" name="message" rows="4" required minlength="20"></textarea>
<p class="error">আরেকটু বিস্তারিত লিখুন।</p>
</div>
<button type="submit">বার্তা পাঠান</button>
</form>শুরুতে কোনো লাল দাগ নেই, যদিও তিনটি ঘরই খালি আর required. ইমেইলে “nafisa” লিখে Tab চাপুন — ঘরের সীমা লাল হয়, নিচে বার্তা আসে। এটাই :user-invalid: সাধারণ :invalid পাতা খোলামাত্র সত্য হতো, আর পাঠক শুরুতেই লাল দাগ দেখে ঘাবড়ে যেতেন। সব ঠিক ভরে “বার্তা পাঠান” চাপুন — প্রিভিউর নিচে লেখা আসবে, ফর্মটি কী পাঠাত। খালি জমা দিলে ব্রাউজারের নিজের বার্তা দেখায় (constraint validation), আর সেই বার্তা screen reader-ও পড়ে। চেষ্টা করুন: দুটি :user-invalid-কে :invalid করে দেখুন পার্থক্য।
আমাদের লাল বার্তাগুলো আপাতত শুধু চোখের জন্য; ব্রাউজারের নিজের বার্তাই কাজের ভার বইছে। লাল রঙের পাশাপাশি লেখাও আছে, কারণ শুধু রং দিয়ে অর্থ বোঝানো চলে না (WCAG ১.৪.১)। :user-invalid ২০২৩ সালের শেষ থেকে সব প্রধান ব্রাউজারে আছে (Baseline); না চিনলে লাল দাগ দেখাবে না, কিন্তু ব্রাউজারের যাচাই ঠিকই চলবে। autocomplete="name" আর "email" ফোনে এক চাপে ঘর ভরার সুযোগ দেয় — ছোট জিনিস, কিন্তু ফর্ম পূরণের হার বাড়ায়।
৭ · Footer, মোশন পলিশ আর রেসপন্সিভ ছবি
Footer ছোট, কিন্তু কাজের: কপিরাইট, অন্য জায়গার লিংক, থিম বাছাই আর “উপরে ফিরুন”। আর পলিশ মানে হালকা মোশন — কার্ডে মাউস রাখলে একটু ওঠে, পাতা খুললে কার্ডগুলো আলতো ভেসে আসে। কিন্তু কিছু মানুষের কাছে নড়াচড়া মাথা ঘোরা বা বমিভাবের কারণ (পর্ব ৪৫)। তাই নিয়ম: মোশন যোগ করুন শুধু তাঁদের জন্য, যাঁরা “কম নড়াচড়া” চাননি।
body { font-family: system-ui, sans-serif; margin: 1rem; }
.list { list-style: none; padding: 0; display: grid; gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
.card { padding: 1rem; border: 1px solid #d4d8de; border-radius: 0.75rem; }
.card h3 { margin: 0 0 0.5rem; }
.card:hover, .card:focus-within {
translate: 0 -4px;
box-shadow: 0 12px 28px oklch(0% 0 0 / 0.12);
}
@media (prefers-reduced-motion: no-preference) {
.card { transition: translate 200ms ease-out, box-shadow 200ms ease-out; }
.reveal { animation: rise 600ms ease-out both; }
.reveal:nth-child(2) { animation-delay: 150ms; }
}
@keyframes rise { from { opacity: 0; transform: translateY(1rem); } }
footer { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center;
justify-content: space-between; margin-top: 2rem; padding-top: 1rem;
border-top: 1px solid #d4d8de; }
footer p, .social { margin: 0; }
.social { display: flex; gap: 1rem; list-style: none; padding: 0; }প্রিভিউ খোলার সময় দুটি কার্ড নিচ থেকে একটু ভেসে ওঠে, দ্বিতীয়টি একটু পরে। কার্ডে মাউস রাখুন, বা “বিস্তারিত দেখুন” লিংকে Tab দিয়ে আসুন (:focus-within) — কার্ড ৪px ওঠে, নিচে ছায়া পড়ে। নিচে footer, ফোনে নিজে থেকে ভাঁজ হয়। সব transition আর animation আছে prefers-reduced-motion: no-preference-এর ভেতরে: যিনি সিস্টেমে “কম নড়াচড়া” চালু করেছেন, তিনি কার্ড ওঠাটা পাবেন হঠাৎ, মসৃণ নড়াচড়া ছাড়া — তথ্য আর ইঙ্গিত থাকে, নড়াচড়া থাকে না। চেষ্টা করুন: no-preference বদলে reduce লিখুন — আপনার মেশিনে কম-নড়াচড়া চালু না থাকলে এখন মোশন বন্ধ দেখাবে। DevTools-এর Rendering প্যানেলে এই পছন্দটি নকল করেও দেখা যায় (পর্ব ৪৭)।
পুরো কোডে আরও দুটি পলিশ: scroll-behavior: smooth (মেনুর লিংকে চাপলে মসৃণ স্ক্রল, সেটাও মোশন-শর্তের ভেতরে), আর @view-transition { navigation: auto; } — পরে আলাদা প্রজেক্ট-পাতা যোগ করলে পাতা বদলের সময় মসৃণ রূপান্তর (cross-document view transition, পর্ব ৩৭)। এটি এখনো সব প্রধান ব্রাউজারে আসেনি — Chromium-ভিত্তিক ব্রাউজার আর Safari-তে চলে (Can I use দেখে নিন); বাকিরা সাধারণভাবে পাতা বদলায়, তাই নিশ্চিন্তে রাখা যায়।
রেসপন্সিভ ছবি — পোর্টফোলিওর সবচেয়ে ভারী অংশ
প্রজেক্টের স্ক্রিনশট আর নিজের ছবিই সাধারণত পাতার সবচেয়ে বড় ফাইল। পর্ব ৩২-এর নিয়মগুলো এখানে টাকার মতো কাজ করে — বাংলাদেশে অনেক পাঠক মোবাইল ডেটায়, প্রতিটি MB-এর দাম আছে:
<!-- প্রথম পর্দার ছবি: আগে আনো, lazy নয় -->
<img src="images/nafisa-480.webp"
srcset="images/nafisa-480.webp 480w, images/nafisa-960.webp 960w"
sizes="(min-width: 48rem) 22rem, 70vw"
width="480" height="480" fetchpriority="high"
alt="ল্যাপটপ হাতে হাসিমুখে নাফিসা রহমান">
<!-- নিচের দিকের প্রজেক্ট-ছবি: দরকার হলে তবেই আনো -->
<picture>
<source type="image/avif" srcset="images/grocery-640.avif 640w, images/grocery-1280.avif 1280w">
<img src="images/grocery-640.webp"
srcset="images/grocery-640.webp 640w, images/grocery-1280.webp 1280w"
sizes="(min-width: 48rem) 22rem, 100vw"
width="640" height="360" loading="lazy" decoding="async" alt="">
</picture>- প্রথম পর্দার ছবি সম্ভবত পাতার LCP — তাই
fetchpriority="high", আর কখনোloading="lazy"নয়। - নিচের ছবি lazy — পাঠক স্ক্রল করে কাছে এলে তবেই আসে।
- সব ছবিতে
widthআরheight— ব্রাউজার আগেই জায়গা রাখে, লেখা লাফায় না (CLS). CSS-এরheight: autoঅনুপাত ঠিক রাখে। - srcset + sizes — ফোন ছোট ফাইল নেয়, বড় পর্দা বড়টি। AVIF/WebP-তে রূপান্তর করুন (Squoosh-এর মতো টুলে), স্ক্রিনশট সাধারণত ১০০–২০০ KB-র নিচে নামানো যায়।
- কার্ডের ছবিতে
alt=""— ঠিক পাশেই শিরোনাম-লিংক একই কথা বলছে; একই কথা দুবার শোনানো বিরক্তিকর। কিন্তু ছবিটি নিজে কোনো তথ্য দিলে (যেমন “আগে আর পরে”), সেটি লিখুন।
৮ · প্রকাশের আগে মান যাচাই — তারপর GitHub Pages
পরীক্ষার খাতা জমা দেওয়ার আগে শেষ পাঁচ মিনিট রিভিশন — বানান, প্রশ্ন নম্বর, কিছু বাদ পড়ল কি না। সাইটেরও তেমনি দুটি চেকলিস্ট আছে। পুরোটা মিলিয়ে নিন:
অ্যাক্সেসিবিলিটি চেকলিস্ট (WCAG 2.2-এর দিকে নজর রেখে)
পারফরম্যান্স চেকলিস্ট
সবুজ হলে প্রকাশ। পর্ব ৫৩-এ পুরো পদ্ধতি দেখেছেন; এখানে সংক্ষেপে ধাপগুলো, একটি জরুরি ফাঁদসহ:
portfolio/ ├── index.html ├── style.css └── images/ $ git init $ git add . $ git commit -m "Portfolio v1: tokens, layout, contact form"
$ git branch -M main $ git remote add origin https://github.com/your-username/portfolio.git $ git push -u origin main
Source: Deploy from a branch
Branch: main / (root) [Save]
Your site is live at https://your-username.github.io/portfolio/
<link rel="stylesheet" href="/style.css"> → https://your-username.github.io/style.css ✗ 404 — CSS নেই! <link rel="stylesheet" href="style.css"> → https://your-username.github.io/portfolio/style.css ✓
$ git add .
$ git commit -m "Add restaurant menu project"
$ git push
… এক-দুই মিনিটে সাইট নিজে থেকে নতুন হয়
- ফাইলগুলো Git-এ। ফোল্ডারে শুধু যা প্রকাশ হবে। ফাইলের নাম ছোট হাতের, হাইফেনে, স্পেস ছাড়া — সার্ভারে
Style.cssআরstyle.cssআলাদা ফাইল। - GitHub-এ পাঠানো। GitHub-এ
portfolioনামে খালি repository বানিয়ে তিন লাইনে push করুন। এখন কোড আপনার কম্পিউটারের বাইরেও নিরাপদ, আর নিয়োগকর্তা চাইলে কোডও দেখতে পারেন। - Pages চালু। Settings → Pages-এ branch
main, ফোল্ডার root. কিছুক্ষণ পর ঠিকানা আসে। প্রথমবার একটু সময় লাগতে পারে — ধৈর্য ধরুন। - সবচেয়ে প্রচলিত ফাঁদ। repository-র নাম
portfolioহলে সাইট থাকে/portfolio/ফোল্ডারে।/style.css(root-relative, পর্ব ৫) তখন ডোমেইনের গোড়ায় খোঁজে — CSS আসে না, সাইট সাদা-কালো। তাই সব path relative:style.css,images/…। (repository-র নামyour-username.github.ioদিলে সাইট গোড়াতেই থাকে।) - আপডেট মানে commit আর push. প্রতিটি নতুন প্রজেক্ট যোগ করে push করলেই সাইট নতুন হয়। নিজের
.comবা.com.bdডোমেইন চাইলে Pages-এর Custom domain ঘরে বসিয়ে DNS ঠিক করুন; Netlify বা cPanel হোস্টিং একই ফাইল নিয়েই চলে (পর্ব ৫৩)।
৯ · পুরো কোড, রুব্রিক আর নিজের রিভিউ
এবার সব component জোড়া লাগানো পুরো সাইট। এটি লাইভ এডিটর নয় — দুটি ফাইল কপি করে VS Code-এ (পর্ব ২) একটি portfolio ফোল্ডারে index.html আর style.css নামে সেভ করুন, পাশে একটি images ফোল্ডারে নিজের ছবি রাখুন, তারপর Live Server দিয়ে খুলুন। প্রতিটি অংশের ওপরে মন্তব্যে লেখা আছে এটি কোন অংশের কাজ।
<!doctype html>
<html lang="bn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>নাফিসা রহমান — ফ্রিল্যান্স ফ্রন্ট-এন্ড ডেভেলপার, খুলনা</title>
<meta name="description" content="ছোট ব্যবসার জন্য দ্রুত, রেসপন্সিভ আর অ্যাক্সেসিবল ওয়েবসাইট। কাজের নমুনা দেখুন, সরাসরি যোগাযোগ করুন।">
<link rel="canonical" href="https://your-username.github.io/portfolio/">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#f7fafe" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0c121a" media="(prefers-color-scheme: dark)">
<meta property="og:type" content="website">
<meta property="og:title" content="নাফিসা রহমান — ফ্রন্ট-এন্ড ডেভেলপার">
<meta property="og:description" content="ছোট ব্যবসার জন্য দ্রুত, রেসপন্সিভ আর অ্যাক্সেসিবল ওয়েবসাইট।">
<meta property="og:url" content="https://your-username.github.io/portfolio/">
<meta property="og:image" content="https://your-username.github.io/portfolio/images/og-cover.png">
<meta property="og:locale" content="bn_BD">
<link rel="icon" href="images/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="style.css">
</head>
<body>
<a class="skip-link" href="#main">সরাসরি মূল অংশে যান</a>
<header class="site-header">
<div class="wrap bar">
<a class="logo" href="./">নাফিসা<span>.dev</span></a>
<button class="menu-btn" type="button" popovertarget="site-menu">মেনু</button>
<nav id="site-menu" class="site-nav" popover aria-label="প্রধান মেনু">
<ul>
<li><a href="#projects">কাজ</a></li>
<li><a href="#about">আমার কথা</a></li>
<li><a href="#contact">যোগাযোগ</a></li>
</ul>
</nav>
</div>
</header>
<main id="main" tabindex="-1">
<section class="hero" aria-labelledby="hero-title">
<div class="wrap hero-grid">
<div>
<p class="eyebrow">ফ্রিল্যান্স ফ্রন্ট-এন্ড ডেভেলপার · খুলনা</p>
<h1 id="hero-title">ছোট ব্যবসার জন্য দ্রুত, সুন্দর আর সবার ব্যবহারযোগ্য ওয়েবসাইট</h1>
<p class="lead">দোকান, কোচিং সেন্টার বা রেস্তোরাঁ — আপনার গ্রাহক যে ফোনেই থাকুন, সাইট খুলবে তাড়াতাড়ি, পড়া যাবে আরামে।</p>
<p class="actions">
<a class="btn" href="#projects">কাজ দেখুন</a>
<a class="btn btn-ghost" href="#contact">কাজের কথা বলুন</a>
</p>
</div>
<img class="hero-photo" src="images/nafisa-480.webp"
srcset="images/nafisa-480.webp 480w, images/nafisa-960.webp 960w"
sizes="(min-width: 48rem) 22rem, 70vw"
width="480" height="480" fetchpriority="high"
alt="ল্যাপটপ হাতে হাসিমুখে নাফিসা রহমান">
</div>
</section>
<section id="projects" class="section" aria-labelledby="projects-title">
<div class="wrap">
<h2 id="projects-title">আমার কাজ</h2>
<ul class="card-grid">
<li>
<article class="card">
<img src="images/grocery-640.webp" width="640" height="360" loading="lazy" decoding="async" alt="">
<div>
<h3><a href="projects/grocery.html">পাড়ার মুদি দোকানের অর্ডার পাতা</a></h3>
<p>এক হাতে ফোনে অর্ডার — বড় বাটন, পরিষ্কার দাম, ধীর নেটেও দ্রুত।</p>
<ul class="tags" aria-label="ব্যবহৃত প্রযুক্তি"><li>HTML</li><li>CSS Grid</li><li>ফর্ম</li></ul>
</div>
</article>
</li>
<li>
<article class="card">
<img src="images/coaching-640.webp" width="640" height="360" loading="lazy" decoding="async" alt="">
<div>
<h3><a href="projects/coaching.html">কোচিং সেন্টারের ফলাফল ও ভর্তি</a></h3>
<p>caption আর scope-সহ অ্যাক্সেসিবল টেবিল, যাচাই-করা ভর্তির ফর্ম।</p>
<ul class="tags" aria-label="ব্যবহৃত প্রযুক্তি"><li>টেবিল</li><li>:user-invalid</li></ul>
</div>
</article>
</li>
<li>
<article class="card">
<img src="images/library-640.webp" width="640" height="360" loading="lazy" decoding="async" alt="">
<div>
<h3><a href="projects/library.html">শাপলা গণপাঠাগারের ওয়েবসাইট</a></h3>
<p>চার পাতার স্ট্যাটিক সাইট — সময়সূচি, ফি আর সদস্য হওয়ার আবেদন।</p>
<ul class="tags" aria-label="ব্যবহৃত প্রযুক্তি"><li>Semantic HTML</li><li>SEO</li></ul>
</div>
</article>
</li>
<li>
<article class="card">
<img src="images/restaurant-640.webp" width="640" height="360" loading="lazy" decoding="async" alt="">
<div>
<h3><a href="projects/restaurant.html">রেস্তোরাঁর মেনু ও ডার্ক মোড</a></h3>
<p>light-dark() দিয়ে দুই থিম, দামের তালিকা ফোনে আরামে পড়া যায়।</p>
<ul class="tags" aria-label="ব্যবহৃত প্রযুক্তি"><li>Theming</li><li>Container query</li></ul>
</div>
</article>
</li>
</ul>
</div>
</section>
<section id="about" class="section section-alt" aria-labelledby="about-title">
<div class="wrap about">
<figure class="about-figure">
<img src="images/workspace-640.webp" width="640" height="800" loading="lazy" decoding="async"
alt="জানালার পাশে ডেস্কে ল্যাপটপ, নোটখাতা আর এক কাপ চা">
<figcaption>খুলনায় আমার কাজের টেবিল</figcaption>
</figure>
<div>
<h2 id="about-title">আমার কথা</h2>
<p>আমি নাফিসা। বিশ্ববিদ্যালয়ে পড়ার পাশাপাশি তিন বছর ধরে দোকান, কোচিং সেন্টার আর সংগঠনের জন্য ওয়েবসাইট বানাচ্ছি। আমার লক্ষ্য একটাই — কম দামি ফোন আর ধীর নেটেও যেন সাইট আরামে চলে।</p>
<h3>যেসব কাজে দক্ষ</h3>
<ul class="chips">
<li>Semantic HTML</li><li>রেসপন্সিভ CSS</li><li>অ্যাক্সেসিবিলিটি (WCAG 2.2)</li>
<li>বাংলা টাইপোগ্রাফি</li><li>পারফরম্যান্স</li><li>Git ও GitHub</li>
</ul>
</div>
</div>
</section>
<section id="contact" class="section" aria-labelledby="contact-title">
<div class="wrap narrow">
<h2 id="contact-title">যোগাযোগ</h2>
<p>নতুন সাইট, পুরোনো সাইট ঠিক করা বা পরামর্শ — দুই কর্মদিবসের মধ্যে উত্তর দিই।</p>
<form class="contact-form" action="https://formspree.io/f/YOUR-FORM-ID" method="post">
<div class="field">
<label for="name">আপনার নাম</label>
<input id="name" name="name" required autocomplete="name">
<p class="error">নামটি লিখুন।</p>
</div>
<div class="field">
<label for="email">ইমেইল</label>
<input id="email" name="email" type="email" required autocomplete="email">
<p class="error">একটি সঠিক ইমেইল লিখুন, যেমন name@example.com</p>
</div>
<div class="field">
<label for="message">কাজের বিবরণ</label>
<textarea id="message" name="message" rows="5" required minlength="20" aria-describedby="message-hint"></textarea>
<p class="hint" id="message-hint">কী ধরনের সাইট, কবে নাগাদ দরকার — অন্তত ২০ অক্ষরে।</p>
<p class="error">আরেকটু বিস্তারিত লিখুন।</p>
</div>
<button class="btn" type="submit">বার্তা পাঠান</button>
</form>
<p>অথবা সরাসরি লিখুন: <a href="mailto:hello@example.com">hello@example.com</a></p>
</div>
</section>
</main>
<footer class="site-footer">
<div class="wrap footer-bar">
<p><small>© ২০২৬ নাফিসা রহমান · খুলনা, বাংলাদেশ</small></p>
<ul class="social" aria-label="অন্য জায়গায় আমি">
<li><a href="https://github.com/your-username">GitHub</a></li>
<li><a href="https://www.linkedin.com/in/your-username">LinkedIn</a></li>
</ul>
<label class="theme-pick">থিম
<select id="theme">
<option value="system">সিস্টেম অনুযায়ী</option>
<option value="light">হালকা</option>
<option value="dark">অন্ধকার</option>
</select>
</label>
<p><a href="#main">উপরে ফিরুন ↑</a></p>
</div>
</footer>
</body>
</html>/* ===== ১. Design tokens ===== */
:root {
color-scheme: light dark;
--bg: light-dark(oklch(98.5% 0.006 250), oklch(18% 0.02 260));
--surface: light-dark(oklch(100% 0 0), oklch(23% 0.025 260));
--text: light-dark(oklch(25% 0.03 260), oklch(95% 0.01 260));
--muted: light-dark(oklch(46% 0.03 260), oklch(76% 0.02 260));
--brand: light-dark(oklch(50% 0.11 160), oklch(78% 0.13 165));
--on-brand: light-dark(oklch(100% 0 0), oklch(20% 0.04 165));
--danger: light-dark(oklch(52% 0.19 25), oklch(75% 0.15 25));
--line: light-dark(oklch(88% 0.01 260), oklch(35% 0.02 260));
--chip: light-dark(oklch(94% 0.03 160), oklch(30% 0.05 165));
--step--1: clamp(0.875rem, 0.85rem + 0.1vw, 0.95rem);
--step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--step-1: clamp(1.25rem, 1.1rem + 0.6vw, 1.5rem);
--step-2: clamp(1.5rem, 1.25rem + 1.2vw, 2rem);
--step-3: clamp(1.75rem, 1.2rem + 3vw, 3.25rem);
--space-2xs: 0.25rem; --space-xs: 0.5rem; --space-s: 0.75rem;
--space-m: 1.25rem; --space-l: 2rem; --space-xl: clamp(3rem, 8vw, 6rem);
--radius-s: 0.5rem; --radius-m: 0.75rem; --radius-pill: 999px;
--wrap: 72rem;
--font: "Noto Sans Bengali", "Hind Siliguri", system-ui, sans-serif;
}
:root:has(#theme option[value="light"]:checked) { color-scheme: light; }
:root:has(#theme option[value="dark"]:checked) { color-scheme: dark; }
/* light-dark() চেনে না এমন পুরোনো ব্রাউজারে অন্তত হালকা থিম */
@supports not (color: light-dark(#fff, #000)) {
:root { --bg: #f7fafe; --surface: #fff; --text: #192230; --muted: #4e5969; --brand: #0b764d;
--on-brand: #fff; --danger: #be222a; --line: #d4d8de; --chip: #dbf2e4; }
}
/* ===== ২. ভিত্তি ===== */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-padding-top: 5rem; }
body { margin: 0; background: var(--bg); color: var(--text); font: var(--step-0)/1.7 var(--font); }
img { display: block; max-width: 100%; height: auto; }
h1, h2, h3 { line-height: 1.2; text-wrap: balance; }
h2 { font-size: var(--step-2); margin-block: 0 var(--space-m); }
h3 { font-size: var(--step-1); }
a { color: var(--brand); text-underline-offset: 0.2em; }
:focus-visible { outline: 3px solid var(--brand); outline-offset: 3px; }
.wrap { width: min(100% - 2 * var(--space-m), var(--wrap)); margin-inline: auto; }
.narrow { --wrap: 40rem; }
.section { padding-block: var(--space-xl); }
.section-alt { background: var(--surface); }
.skip-link { position: absolute; inset-inline-start: var(--space-s); top: -4rem; z-index: 10;
padding: var(--space-xs) var(--space-s); border-radius: var(--radius-s);
background: var(--brand); color: var(--on-brand); }
.skip-link:focus { top: var(--space-s); }
/* ===== ৩. Header আর মেনু ===== */
.site-header { position: sticky; top: 0; z-index: 5; background: var(--bg); border-bottom: 1px solid var(--line); }
.bar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-m); padding-block: var(--space-s); }
.logo { font-size: var(--step-1); font-weight: 700; color: var(--text); text-decoration: none; }
.logo span { color: var(--brand); }
.menu-btn { font: inherit; padding: var(--space-xs) var(--space-s); color: var(--text); background: var(--surface);
border: 1px solid var(--muted); border-radius: var(--radius-s); }
.site-nav ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2xs); }
.site-nav a { display: block; padding: var(--space-xs) var(--space-s); color: var(--text);
text-decoration: none; border-radius: var(--radius-s); }
.site-nav a:hover { background: var(--chip); }
.site-nav:popover-open { inset: 4rem var(--space-m) auto auto; margin: 0; min-width: 12rem; padding: var(--space-s);
color: var(--text); background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius-m); box-shadow: 0 12px 32px oklch(0% 0 0 / 0.15); }
@supports not selector(:popover-open) { .menu-btn { display: none; } }
@media (width >= 48rem) {
.menu-btn { display: none; }
.site-nav { display: block; position: static; margin: 0; padding: 0; border: 0; background: none; overflow: visible; }
.site-nav ul { display: flex; }
}
/* ===== ৪. Hero ===== */
.hero { padding-block: var(--space-xl);
background: radial-gradient(circle at 85% 15%, color-mix(in oklch, var(--brand) 25%, transparent), transparent 45%); }
.hero-grid { display: grid; gap: var(--space-l); align-items: center; }
@media (width >= 48rem) { .hero-grid { grid-template-columns: 1fr 22rem; } }
.eyebrow { margin: 0; font-weight: 600; color: var(--brand); }
.hero h1 { font-size: var(--step-3); max-inline-size: 22ch; margin-block: var(--space-xs) var(--space-m); }
.lead { font-size: var(--step-1); color: var(--muted); max-inline-size: 60ch; }
.hero-photo { width: min(70vw, 22rem); aspect-ratio: 1; object-fit: cover; border-radius: 50%; justify-self: center; }
.actions { display: flex; flex-wrap: wrap; gap: var(--space-s); }
.btn { display: inline-block; padding: var(--space-s) var(--space-m); border: 0; border-radius: var(--radius-m);
font: inherit; font-weight: 600; color: var(--on-brand); background: var(--brand); text-decoration: none; cursor: pointer; }
.btn-ghost { color: var(--brand); background: transparent; box-shadow: inset 0 0 0 2px var(--brand); }
/* ===== ৫. প্রজেক্ট কার্ড ===== */
.card-grid { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-m);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); }
.card-grid > li { container-type: inline-size; }
.card { position: relative; display: grid; gap: var(--space-s); height: 100%; padding: var(--space-s);
background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-m); }
.card img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: var(--radius-s); }
.card h3 { margin: 0 0 var(--space-2xs); }
.card h3 a { color: var(--text); text-decoration: none; }
.card h3 a::after { content: ""; position: absolute; inset: 0; }
.card h3 a:focus-visible { outline: none; }
.card:has(a:focus-visible) { outline: 3px solid var(--brand); outline-offset: 3px; }
.card p { margin: 0; color: var(--muted); }
.tags { display: flex; flex-wrap: wrap; gap: var(--space-2xs); list-style: none; margin: var(--space-xs) 0 0; padding: 0; font-size: var(--step--1); }
.tags li { padding: 0 var(--space-xs); border-radius: var(--radius-pill); background: var(--chip); }
@container (width > 24rem) {
.card { grid-template-columns: 10rem 1fr; align-items: start; }
}
/* ===== ৬. About আর দক্ষতা ===== */
.about { display: grid; gap: var(--space-l); align-items: center;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); }
.about-figure { margin: 0; }
.about-figure img { border-radius: var(--radius-m); }
figcaption { margin-top: var(--space-xs); font-size: var(--step--1); color: var(--muted); }
.chips { display: flex; flex-wrap: wrap; gap: var(--space-xs); list-style: none; padding: 0; }
.chips li { padding: var(--space-2xs) var(--space-s); border-radius: var(--radius-pill); background: var(--chip); font-size: var(--step--1); }
/* ===== ৭. যোগাযোগ ফর্ম ===== */
.contact-form { display: grid; gap: var(--space-m); }
.field { display: grid; gap: var(--space-2xs); }
.field label { font-weight: 600; }
input, textarea, select { font: inherit; color: var(--text); background: var(--surface);
padding: var(--space-xs) var(--space-s); border: 1px solid var(--muted); border-radius: var(--radius-s); }
.hint { margin: 0; font-size: var(--step--1); color: var(--muted); }
.error { display: none; margin: 0; font-size: var(--step--1); color: var(--danger); }
.field:has(:user-invalid) :is(input, textarea) { border: 2px solid var(--danger); }
.field:has(:user-invalid) .error { display: block; }
/* ===== ৮. Footer ===== */
.site-footer { padding-block: var(--space-l); border-top: 1px solid var(--line); }
.footer-bar { display: flex; flex-wrap: wrap; gap: var(--space-m); align-items: center; justify-content: space-between; }
.footer-bar p { margin: 0; }
.social { display: flex; gap: var(--space-m); list-style: none; margin: 0; padding: 0; }
.theme-pick select { margin-inline-start: var(--space-2xs); }
/* ===== ৯. মোশন — যিনি কম নড়াচড়া চান, তাঁর জন্য বন্ধ ===== */
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
.card { transition: translate 200ms ease-out, box-shadow 200ms ease-out; }
.card:hover { translate: 0 -4px; box-shadow: 0 12px 28px oklch(0% 0 0 / 0.12); }
@view-transition { navigation: auto; }
}কোড পড়ার সময় তিনটি জিনিস খুঁজুন। এক, component-এ সরাসরি রং বা ফাঁকের সংখ্যা প্রায় নেই (ছায়ার মতো দু-একটি ছাড়া) — সবই token. দুই, পর্দার মাপের media query মাত্র দুটি (মেনু আর hero), বাকি সব রেসপন্সিভনেস আসে auto-fit, clamp(), min(), flex-wrap আর একটি container query থেকে — বিখ্যাত “কম breakpoint, বেশি নমনীয়তা” ভাবনা। তিন, প্রতিটি নতুন ফিচারের পেছনে একটি নিরাপদ পথ: popover না থাকলে সাধারণ তালিকা, light-dark() না থাকলে হালকা থিম, :user-invalid না থাকলে ব্রাউজারের নিজের যাচাই।
রুব্রিক — নিজেকে নম্বর দিন (মোট ১০০)
| মাপকাঠি | নম্বর | পূর্ণ নম্বর পেতে যা লাগবে |
|---|---|---|
| পরিকল্পনা | ১০ | লক্ষ্য, পাঠক, content inventory, সাইটম্যাপ আর wireframe লেখা আছে; লেখা আসল, Lorem ipsum নেই |
| Design system | ১৫ | রং, লেখা, ফাঁক, কোণ — সব token; দুই থিম; contrast মাপা ও শর্ত পূরণ |
| HTML কাঠামো | ১৫ | landmark, skip link, একটি h1, heading-এর ক্রম; পূর্ণ head, OG; validator-এ শূন্য error |
| রেসপন্সিভ লেআউট | ১৫ | 320px থেকে বড় পর্দা পর্যন্ত পাশে স্ক্রল নেই; auto-fit grid আর অন্তত একটি container query |
| Component | ১৫ | মেনু কি-বোর্ডে চলে, কার্ড পুরোটা চাপা যায়, ফর্মে label-autocomplete-যাচাই |
| অ্যাক্সেসিবিলিটি | ১৫ | চেকলিস্টের সব ঘর; screen reader দিয়ে একবার শোনা |
| পারফরম্যান্স ও পলিশ | ১০ | Lighthouse mobile-এ চারটিই সবুজ; ছবি অপ্টিমাইজ; মোশন শুধু no-preference-এ |
| প্রকাশ | ৫ | লাইভ লিংক, সব path relative, README-তে কী কী ব্যবহার করেছেন |
৮০-এর ওপরে পেলে সাইটটি ক্লায়েন্টকে পাঠানোর মতো। যে সারিতে নম্বর কাটা গেল, পাশের তালিকার পর্বটি আবার দেখুন। আর একজন বন্ধুকে আপনার ফোনে সাইটটি দিয়ে বলুন, “৩০ সেকেন্ডে বলো আমি কী করি” — এর চেয়ে সৎ রিভিউ হয় না।
নিজের রিভিউ — জমা দেওয়ার আগে শেষবার
১০ · AI-এর যুগে — AI-কে দিয়ে পোর্টফোলিও বানালে কী যাচাই করবেন
“আমার জন্য একটা পোর্টফোলিও ওয়েবসাইট বানাও” — AI এক মিনিটে চকচকে একটি পাতা দেবে। সমস্যা হলো, চোখে সুন্দর দেখানো আর ঠিক হওয়া এক নয়। AI শিখেছে ইন্টারনেটের কোটি পাতা থেকে, যার বড় অংশ পুরোনো অভ্যাসে লেখা। আর ক্লায়েন্ট যখন আপনার সাইট খুলবেন, তিনি যাচাই করছেন আপনার দক্ষতা — AI-এর নয়।
| AI-এর ভুল | কেন সমস্যা | ঠিক উপায় (পর্ব) |
|---|---|---|
div-এ onclick দিয়ে হ্যামবার্গার মেনু | কি-বোর্ডে চলে না, screen reader বাটন চেনে না | <button popovertarget> (১১) |
| প্রতিটি নিয়মে আলাদা hex রং, কোনো token নেই | থিম বদলানো অসম্ভব, contrast কেউ মাপেনি | custom property token (৩৩) |
dark mode = filter: invert(1) | ছবি উল্টে যায়, রং কটকটে | light-dark() + আলাদা মান (৩৬) |
৫–৬টি max-width breakpoint, ডেস্কটপ-আগে | বাড়তি CSS, মাঝের মাপে ভাঙে | মোবাইল-ফার্স্ট, auto-fit, clamp (৩০, ৩৫) |
| ছবিতে width-height নেই, প্রথম ছবিও lazy | লেখা লাফায়, প্রথম পর্দা দেরিতে আসে | width-height, fetchpriority (৩২, ৪৯) |
| এক পাতার জন্য পুরো CSS framework-এর CDN | অপ্রয়োজনীয় ভারী ফাইল | নিজের ছোট CSS (৫১) |
| বানানো ক্লায়েন্ট-লোগো, ভুয়া রিভিউ | অসৎ — ধরা পড়লে বিশ্বাস শেষ | শুধু আসল কাজ, আসল মতামত |
AI-কে সহকারী হিসেবে কাজে লাগানোর ভালো উপায় — পুরো সাইট নয়, একটি একটি component চান, শর্তসহ। আর শেষে তাকে নিজের কাজের দুর্বলতা বলতে বলুন:
AI-এর কোড সাইটে বসানোর আগে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ফ্রন্ট-এন্ড ইন্টারভিউতে, বিশেষ করে পোর্টফোলিও দেখে, এ ধরনের প্রশ্ন প্রায়ই আসে:
- Media query আর container query-র পার্থক্য? — media query পুরো পর্দা (viewport) দেখে, container query একটি নির্দিষ্ট পূর্বপুরুষ element-এর মাপ দেখে; reusable component-এর জন্য দ্বিতীয়টি।
auto-fitআরauto-fill-এর পার্থক্য? — দুটোই যত কলাম আঁটে বানায়; জায়গা বেশি থাকলে auto-fill খালি কলাম রাখে, auto-fit সেগুলো গুটিয়ে আসল item ছড়িয়ে দেয়।- Design token কী, কেন? — নকশার সিদ্ধান্তের (রং, মাপ, ফাঁক) নাম দেওয়া মান; একবার বদলালে সব জায়গায় বদলায়, থিম সহজ হয়।
- Skip link কেন দরকার? — কি-বোর্ড ও screen reader ব্যবহারকারী প্রতিটি পাতায় বারবার মেনু পার না হয়ে সরাসরি মূল অংশে যেতে পারেন।
- আপনার সাইটের Lighthouse স্কোর কত, কী ঠিক করেছিলেন? — সৎ সংখ্যা আর একটি নির্দিষ্ট উন্নতির গল্প বলুন (যেমন ছবি AVIF করে LCP কমানো)।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে পাতা-বানানো থেকে নকশা-ভাবনায় নিয়ে যাবে।
প্র ০১Design token-এর ধারণা কোথা থেকে এলো? ছোট একটি পোর্টফোলিওতেও কি এত আয়োজন দরকার?
“Design token” নামটি জনপ্রিয় হয় ২০১০-এর দশকের মাঝামাঝি, Salesforce-এর Lightning Design System-এর কাজ থেকে। সমস্যাটি ছিল বাস্তব: একই কোম্পানির ওয়েব, iOS আর Android অ্যাপে “ব্র্যান্ডের নীল” তিন জায়গায় তিনভাবে লেখা — কোথাও hex, কোথাও RGB, কোথাও ডিজাইনারের চোখের আন্দাজে। একটি রং বদলাতে হলে কয়েক দলকে কয়েক সপ্তাহ খুঁজতে হতো। সমাধান: সিদ্ধান্তটিকে একটি নাম দাও (color-brand), মান রাখো এক জায়গায়, আর একটি টুল সেখান থেকে প্রতিটি প্ল্যাটফর্মের কোড বানাক। পরে W3C-র একটি Community Group (Design Tokens Community Group) token রাখার একটি সাধারণ ফাইল-ফরম্যাট দাঁড় করানোর কাজ শুরু করে, যাতে Figma-র মতো নকশার টুল আর কোড একই ভাষায় কথা বলে।
দুই স্তরের token-এর ধারণাটিও এখান থেকে: নিচে কাঁচা মান (“সবুজ-৫০০”), ওপরে অর্থ (semantic token — --brand, --danger). component শুধু অর্থের নাম জানে। তাই dark mode-এ শুধু অর্থের নামের নিচের মানটি বদলায়, component-এর একটি লাইনও নয়।
ছোট সাইটে দরকার আছে? পুরো টুলচেইন নয়, কিন্তু ধারণাটি অবশ্যই। দশটি token আর দুটি থিম লিখতে আধা ঘণ্টা লাগে; বদলে পাওয়া যায় তিনটি জিনিস — পুরো সাইটের রং-বদল এক লাইনে, contrast একবার মেপে নিশ্চিন্ত থাকা, আর একটি সুসংগত চেহারা যা ক্লায়েন্টের চোখে “পেশাদার” লাগে। বড় দলের কোডও এই একই ভাবনায় লেখা, তাই ইন্টারভিউতে এটি দেখাতে পারলে বোঝা যায় আপনি দলের সাথে কাজ করার মতো করে ভাবেন। উল্টো দিকে সাবধানতা: ৩০০টি token বানিয়ে ফেলা আরেক রকম জট — যে মান দুই জায়গায় ব্যবহার হয় না, তার token দরকার নেই।
মূল উপলব্ধি: token হলো সিদ্ধান্তকে মানের বদলে নাম দেওয়া। নামের পেছনের মান বদলায়, নাম থাকে — আর এই স্থিরতাই থিম, ব্র্যান্ড-বদল আর দলের কাজকে সহজ করে।
প্র ০২রেসপন্সিভ ডিজাইন ২০১০ সালের ধারণা — তাহলে container query আসতে এত বছর লাগল কেন?
২০১০ সালে Ethan Marcotte “A List Apart”-এ “Responsive Web Design” প্রবন্ধে তিনটি উপাদান জুড়ে দেন — নমনীয় grid, নমনীয় ছবি আর media query. প্রায় একই সময়ে Luke Wroblewski “Mobile First” ভাবনার কথা বলেন: ছোট পর্দা দিয়ে শুরু করলে বাধ্য হয়ে আসল জরুরি জিনিস বেছে নিতে হয়। এরপর এক দশক ধরে আমরা পর্দার মাপ দেখে লেআউট বদলেছি।
কিন্তু component-ভিত্তিক কাজ যত বাড়ল, সমস্যাও চোখে পড়ল: একটি কার্ড পাতার মাঝখানে চওড়া, sidebar-এ সরু — অথচ পর্দা একই। ডেভেলপাররা বছরের পর বছর “element query” চেয়েছেন, কিন্তু ব্রাউজার-নির্মাতারা একটি মৌলিক বাধা দেখাতেন: যদি একটি element-এর CSS তার নিজের মাপের ওপর নির্ভর করে, আর সেই CSS-ই মাপ বদলে দেয়, তাহলে হিসাব চক্রে পড়তে পারে — বড় হলে ছোট করার নিয়ম, ছোট হলে বড় করার নিয়ম, অনন্ত লুপ।
সমাধান এলো containment-এর ধারণা থেকে। container-type: inline-size লিখে আপনি ব্রাউজারকে প্রতিশ্রুতি দেন: “এই বাক্সের চওড়া তার ভেতরের জিনিস দিয়ে ঠিক হবে না।” ফলে ভেতরের element-এর নিয়ম বাক্সের চওড়া বদলাতে পারে না, চক্রও তৈরি হয় না। এজন্যই container নিজে নিজের query-তে বদলায় না, শুধু ভেতরের element বদলায় — অংশ ৫-এ ঠিক এটাই দেখেছেন। এই নকশা পাকা হওয়ার পর ২০২২–২৩ সালের মধ্যে প্রধান ব্রাউজারগুলো size container query চালু করে।
তাহলে media query কি অচল? না। পাতার বড় কাঠামো — মেনু এক সারিতে না বাটনে, hero দুই কলামে কি না — পর্দার ব্যাপার, সেখানে media query-ই ঠিক। আর prefers-color-scheme, prefers-reduced-motion-এর মতো ব্যবহারকারীর পছন্দ জানার উপায় শুধু media query.
মূল উপলব্ধি: পাতার কাঠামো পর্দা দেখে, component নিজের ঘর দেখে। একটি নতুন CSS ফিচার কেন দেরিতে আসে, তা বুঝলে বোঝা যায় তার নিয়মগুলো কেন এমন — আর ভুলও কম হয়।
প্র ০৩সবাই React বা Next.js শিখছে — তাহলে পোর্টফোলিও শুধু HTML আর CSS দিয়ে কেন?
২০০৬ সালে W3C-র Technical Architecture Group (TAG) “The Rule of Least Power” নামে একটি নীতি প্রকাশ করে, যার ভাবনা Tim Berners-Lee-র: কোনো কাজের জন্য সবচেয়ে কম ক্ষমতার যে ভাষা যথেষ্ট, সেটিই বেছে নাও। কারণ কম ক্ষমতার ভাষা (যেমন HTML) অন্য প্রোগ্রাম সহজে বোঝে — সার্চ ইঞ্জিন, screen reader, অনুবাদ টুল, এমনকি AI-এর পড়ার টুলও। আর তা ভাঙেও কম।
একটি পোর্টফোলিওর কাজ লেখা, ছবি, লিংক আর একটি ফর্ম দেখানো — এর কোনোটির জন্য JavaScript framework লাগে না। framework-এ বানালে সাধারণত শুরুতেই কয়েকশো KB JavaScript আসে, যা কম দামি ফোনে পার্স করে চালাতে সময় লাগে; ধীর মোবাইল ডেটায় প্রথম পর্দা দেরিতে আসে। অথচ আমাদের দুটি ফাইলের সাইট শুধু HTML আর CSS — সবচেয়ে সস্তা ফোনেও দ্রুত। একসময় যেসব কাজে JavaScript লাগত, সেগুলো এখন HTML-CSS নিজেই করে: details, dialog, popover, :has(), :user-invalid, container query, view transition.
এর মানে framework খারাপ নয়। অনেক পাতা, লগইন, ড্যাশবোর্ড, বারবার বদলানো ডেটা — সেখানে React-এর মতো টুল আসলেই সময় বাঁচায় (JavaScript কোর্সের ভিত্তি শেষ করে সেই পথে যাওয়া যায়)। আবার Astro, Eleventy-র মতো static site generator-ও আছে, যারা component-এর সুবিধা দিয়েও শেষে সাধারণ HTML বানায় — পোর্টফোলিও বড় হলে সেটি ভালো পরের ধাপ।
আর একটি পেশাগত দিক: নিয়োগকর্তা জানেন framework দুই মাসে শেখা যায়, কিন্তু semantic HTML, cascade, লেআউট আর অ্যাক্সেসিবিলিটির গভীর জ্ঞান বছর লাগে — আর সব framework-এর নিচে এগুলোই চলে। হাতে বানানো, দ্রুত, অ্যাক্সেসিবল একটি সাইট সেই ভিত্তির প্রমাণ।
মূল উপলব্ধি: কাজের মাপে টুল। “সবাই ব্যবহার করে” টুল বাছাইয়ের কারণ নয়; কারণ হলো পাঠকের ফোন, নেট আর প্রয়োজন।
প্র ০৪একজন বাংলাদেশি ফ্রিল্যান্সারের সাইটে অ্যাক্সেসিবিলিটি কি শুধু ভালো কাজ, নাকি ব্যবসার প্রয়োজনও?
দুটোই, আর দ্বিতীয়টি দিন দিন বড় হচ্ছে। WebAIM নামের একটি প্রতিষ্ঠান প্রতি বছর জনপ্রিয় দশ লাখ ওয়েবসাইটের হোমপেজ স্বয়ংক্রিয়ভাবে পরীক্ষা করে (“WebAIM Million”); কয়েক বছর ধরে প্রায় ৯৫ শতাংশ হোমপেজেই যন্ত্রে ধরা পড়ার মতো WCAG-ভুল পাওয়া যাচ্ছে — সবচেয়ে বেশি কম contrast, alt ছাড়া ছবি, label ছাড়া ফর্ম আর নামহীন লিংক। অর্থাৎ এই কোর্সে যা শিখেছেন, বাজারে তা দুর্লভ দক্ষতা।
আইনের দিকও আছে। W3C ২০২৩ সালের অক্টোবরে WCAG 2.2 প্রকাশ করে, যাতে focus ঢাকা না পড়া, টেনে-আনার বিকল্প আর চাপার জায়গার ন্যূনতম মাপের মতো নতুন শর্ত যোগ হয়। ইউরোপীয় ইউনিয়নের European Accessibility Act ২০২৫ সালের জুন থেকে অনেক ডিজিটাল পণ্য ও সেবায় প্রযোজ্য হয়েছে; যুক্তরাষ্ট্রে ADA-কে ঘিরে ওয়েবসাইট নিয়ে মামলা নিয়মিত ঘটনা। আপনার বিদেশি ক্লায়েন্টের অনলাইন দোকান যদি এসব নিয়মের আওতায় পড়ে, তাঁর দরকার এমন ডেভেলপার যিনি শুরু থেকেই অ্যাক্সেসিবল কোড লেখেন।
দেশের বাস্তবতাও ভাবুন: দৃষ্টি বা চলাফেরায় অক্ষম মানুষ, বয়স্ক বাবা-মা যাঁরা বড় লেখা চালু রাখেন, রোদে ফোন দেখা রিকশাচালক, ভাঙা স্ক্রিনের ফোন — সবাই একই সাইট ব্যবহার করেন। contrast ঠিক থাকলে রোদে পড়া যায়; বড় বাটন ভাঙা স্ক্রিনেও চাপা যায়। একে বলে curb-cut effect: একজনের জন্য বানানো সুবিধা সবার কাজে লাগে।
পোর্টফোলিওতে কীভাবে দেখাবেন? প্রতিটি প্রজেক্টের বিবরণে এক লাইন: “Lighthouse Accessibility ১০০, কি-বোর্ড আর NVDA দিয়ে পরীক্ষিত।” অনেক ক্লায়েন্ট এই লাইনটিই খোঁজেন।
মূল উপলব্ধি: অ্যাক্সেসিবিলিটি নৈতিকতা, আইন আর মান — তিনটিই। যে দক্ষতা দুর্লভ আর চাহিদায় বাড়ছে, সেটিই ফ্রিল্যান্সারের দাম বাড়ায়।
প্র ০৫Dark mode কি শুধু রং উল্টে দেওয়া? oklch আর light-dark() কেন এই কাজে ভালো?
Dark mode ব্যাপকভাবে জনপ্রিয় হয় ২০১৮–১৯ সালে, যখন macOS, তারপর iOS আর Android সিস্টেম-জুড়ে অন্ধকার থিম চালু করে। ওয়েব যোগ দেয় prefers-color-scheme media feature দিয়ে, ২০১৯–২০ সালের মধ্যে প্রধান ব্রাউজারগুলোতে। প্রথম যুগে অনেক সাইট dark mode বানাত পুরো পাতায় filter: invert(1) দিয়ে — ফলাফল: ছবি নেগেটিভের মতো, ব্র্যান্ডের সবুজ হয়ে যায় গোলাপি।
ভালো dark mode হলো আলাদা একটি নকশা। তিনটি নিয়ম: পটভূমি একেবারে কালো নয়, সামান্য রঙিন গাঢ় (চোখে আরাম, OLED-এ “smear” কম); লেখা একেবারে সাদা নয়; আর উজ্জ্বল রঙের তীব্রতা একটু কমানো, নইলে অন্ধকারে চোখে লাগে। নাফিসার থিমে তাই অন্ধকারের brand সবুজ উজ্জ্বলতায় ৭৮%, কিন্তু বাটনের লেখা গাঢ় — উল্টো সম্পর্ক।
এখানেই oklch-এর সুবিধা। Björn Ottosson ২০২০ সালে Oklab color space প্রকাশ করেন, যেখানে সমান সংখ্যার পার্থক্য চোখেও প্রায় সমান দেখায়; CSS Color 4-এ এর মেরু রূপ oklch() যোগ হয়। পুরোনো hsl()-এ “lightness 50%” হলুদ আর নীলে চোখে একেবারে আলাদা উজ্জ্বল দেখায় — তাই hue বদলালেই contrast গোলমাল হয়। oklch-এ L ঠিক রেখে hue বদলালে উজ্জ্বলতা প্রায় একই থাকে, তাই দুই থিমের জন্য দুই সারি L ঠিক করে নিলেই চলে।
light-dark() এসেছে লেখার ঝামেলা কমাতে: আগে প্রতিটি token দুবার লিখতে হতো — একবার সাধারণভাবে, একবার @media (prefers-color-scheme: dark)-এর ভেতরে, আর পাঠকের হাতের থিম-বাছাই যোগ করলে তৃতীয়বার। এখন এক লাইনে দুটি মান, আর কোনটি চলবে তা ঠিক করে color-scheme — যা সিস্টেম থেকেও আসতে পারে, পাঠকের বাছাই থেকেও। সাথে ব্রাউজারের নিজের অংশও (scrollbar, ফর্মের ঘর) সঠিক থিম পায়।
মূল উপলব্ধি: dark mode মানে উল্টানো নয়, দ্বিতীয় একটি সুচিন্তিত রং-সারি। ভালো রং-মডেল আর ঠিক টুল নকশার সিদ্ধান্তকে গণিতের মতো নির্ভরযোগ্য করে।
অনুশীলন — সাইটটিকে আরও এগিয়ে নিন
প্রতিটি কাজ মূল সাইটের একটি extension. একটি একটি করে যোগ করুন, প্রতিবার যাচাই করে push করুন।
-
থিমের পছন্দ মনে রাখা: এখন footer-এর থিম বাছাই পাতা reload করলেই হারিয়ে যায়। কীভাবে মনে রাখবেন, আর পাতা খোলার মুহূর্তে এক ঝলক ভুল থিম দেখানো (theme flash) কীভাবে এড়াবেন?
ছোট একটি JavaScript দিয়ে বাছাইটি
localStorage-এ রাখুন, আর পাতা খুললে ফিরিয়ে বসান। CSS-এর:has()নিয়ম বদলাতে হবে না — select-এর মান বদলালেই থিম বদলায়:<script> // পছন্দ মনে রাখা: পাতা খুললেই আগের থিম বসাও const pick = document.querySelector('#theme'); try { pick.value = localStorage.getItem('theme') || 'system'; } catch (e) {} pick.addEventListener('change', () => { try { localStorage.setItem('theme', pick.value); } catch (e) {} }); </script>স্ক্রিপ্টটি
</body>-এর ঠিক আগে নয়, footer-এর select-এর ঠিক পরে রাখুন — যত আগে চলে, ভুল থিম তত কম সময় দেখা যায়।try…catchএজন্য যে কিছু ব্রাউজারে (private মোড, ব্লক করা storage)localStorageerror দেয়; তখন শুধু সিস্টেমের থিম চলবে, সাইট ভাঙবে না। JavaScript একেবারে বন্ধ থাকলেও সিস্টেমের থিম ঠিকই আসে — progressive enhancement. -
JavaScript ছাড়া প্রজেক্ট ফিল্টার: “সব / দোকান / শিক্ষা” — তিনটি বাছাই দিয়ে কার্ড ছাঁকতে চান। শুধু HTML আর CSS দিয়ে কীভাবে করবেন, আর এর অ্যাক্সেসিবিলিটির দিক কী?
রেডিও বাটনের দল আর
:has()। প্রতিটি কার্ডের<li>-এ একটিdata-cat:<fieldset class="filter"> <legend>দেখান:</legend> <label><input type="radio" name="f" value="all" checked> সব</label> <label><input type="radio" name="f" value="shop"> দোকান</label> <label><input type="radio" name="f" value="edu"> শিক্ষা</label> </fieldset> <!-- প্রতিটি কার্ডের li-তে data-cat="shop" বা data-cat="edu" -->body:has(.filter [value="shop"]:checked) .card-grid > li:not([data-cat="shop"]), body:has(.filter [value="edu"]:checked) .card-grid > li:not([data-cat="edu"]) { display: none; }রেডিও বাটন আসল form control, তাই কি-বোর্ডে তীর-চিহ্নে চলে, আর fieldset-legend দলটির নাম দেয়।
display: noneলুকানো কার্ড screen reader থেকেও সরায় — ঠিক যা চাই। একটি উন্নতি: কয়টি কার্ড দেখাচ্ছে তা একটিaria-liveঅংশে জানাতে চাইলে সামান্য JavaScript লাগবে।:has()না চিনলে সব কার্ড দেখায় — কিছু হারায় না। -
Case study পাতা আর মসৃণ রূপান্তর: প্রতিটি প্রজেক্টের জন্য
projects/grocery.htmlপাতা বানান — সমস্যা, আপনার সমাধান, ফলাফল। কার্ডের ছবি থেকে নতুন পাতার বড় ছবিতে মসৃণভাবে “উড়ে যাওয়া” কীভাবে করবেন?দুই পাতাতেই
@view-transition { navigation: auto; }(পুরো CSS-এ আছেই), আর দুই ছবিতে একইview-transition-name:/* style.css — index.html আর প্রজেক্টের পাতা, দুই জায়গাতেই */ @media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; } } <!-- index.html: মুদি দোকানের কার্ডের ছবি --> <img src="images/grocery-640.webp" style="view-transition-name: cover-grocery" …> <!-- projects/grocery.html: একই ছবির বড় রূপ, একই নাম --> <img src="../images/grocery-1280.webp" style="view-transition-name: cover-grocery" …>একই origin-এর দুই পাতা হলেই কাজ করে। নাম পুরো পাতায় অনন্য হতে হবে — চারটি কার্ডে চারটি আলাদা নাম। এটি এখনো সব প্রধান ব্রাউজারে আসেনি; না চিনলে সাধারণভাবে পাতা বদলায়। case study-র পাতায় নতুন
<title>, description, OG আর../style.css— path-এ../ভুলবেন না, নইলে CSS আসবে না। -
প্রিন্টযোগ্য CV: অনেক নিয়োগকর্তা সাইটটিই PDF করে রাখেন (Ctrl+P). কোন অংশ প্রিন্টে লুকাবেন, আর লিংকের ঠিকানা কাগজে কীভাবে দেখাবেন?
একটি
@media printঅংশ যোগ করুন:@media print { .site-header, .site-footer, .contact-form, .actions { display: none; } body { background: #fff; color: #000; font-size: 11pt; } .section { padding-block: 1rem; } .card { break-inside: avoid; border-color: #999; } a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; } }মেনু, ফর্ম, বাটন কাগজে অকেজো — লুকান। কাগজে লিংকে চাপা যায় না, তাই
attr(href)দিয়ে ঠিকানা পাশে ছাপুন।break-inside: avoidএকটি কার্ডকে দুই পাতায় ভাঙতে দেয় না। পটভূমি সাদা আর লেখা কালো রাখলে কালি বাঁচে, পড়াও সহজ। DevTools-এর Rendering প্যানেলে “Emulate CSS media type: print” বেছে প্রিন্ট না করেই দেখা যায়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি পোর্টফোলিও সাইট বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📝 লক্ষ্য, পাঠক, content inventory আর সাইটম্যাপ লেখা
- 🎨 রং, লেখার মাপ আর ফাঁকের design token ঠিক করা
- 🦴 landmark আর skip link-সহ HTML কঙ্কাল
- 🧩 একটি একটি করে component বানানো ও পরীক্ষা
- ✅ অ্যাক্সেসিবিলিটি-পারফরম্যান্স চেকলিস্ট আর Lighthouse
- 🚀 Git দিয়ে push করে GitHub Pages-এ প্রকাশ
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr))-এ min(100%, 15rem) কেন লেখা হয়?
- যাতে সবসময় ঠিক ১৫টি কলাম হয়
- যাতে পাত্র 15rem-এর চেয়ে সরু হলেও কলাম পাত্রের বাইরে উপচে না পড়ে
- যাতে কলাম কখনো 15rem-এর চেয়ে বড় না হয়
- পুরোনো ব্রাউজারে grid চালু করার জন্য
একটি token লেখা আছে --bg: light-dark(white, black), কিন্তু সিস্টেম অন্ধকারে থাকলেও পটভূমি সাদাই থাকছে। সবচেয়ে সম্ভাব্য কারণ কী?
- light-dark()-এ নামকরা রং (white, black) চলে না
- custom property-র ভেতরে light-dark() লেখা যায় না
- কোথাও
color-scheme: light darkলেখা হয়নি - light-dark() শুধু @media (prefers-color-scheme: dark)-এর ভেতরে কাজ করে
একই প্রজেক্ট-কার্ড পাতার চওড়া মূল অংশে আর সরু sidebar-এ বসে। কার্ডের ভেতরের লেআউট তার নিজের চওড়া দেখে বদলাতে কোনটি ব্যবহার করবেন?
- কার্ডের পাত্রে container-type দিয়ে @container query
- @media (width > 40rem)
- @media (prefers-color-scheme: dark)
- কার্ডের সব মাপ vw এককে লেখা
মোবাইল মেনুতে <button popovertarget="menu"> আর <nav id="menu" popover> ব্যবহার করলে JavaScript ছাড়াই কী পাওয়া যায়?
- মেনুর লিংকে চাপলে মেনু নিজে থেকে বন্ধ হওয়া
- বড় পর্দায় লিংকগুলো আপনা-আপনি এক সারিতে চলে আসা
- মেনু খোলা থাকলে বাকি পাতা পুরোপুরি অচল (modal) হয়ে যাওয়া
- বাটনে খোলা-বন্ধ, top layer-এ দেখানো, আর Esc বা বাইরে চাপলে বন্ধ হওয়া
পাতার একদম ওপরের hero ছবির জন্য কোন attribute-গুচ্ছটি সবচেয়ে ভালো?
loading="lazy", width-height ছাড়াwidth,height,srcset,sizesআরfetchpriority="high"- শুধু
decoding="async" - ছবিটি CSS-এর background-image করে দেওয়া
সাইট প্রকাশ হলো https://rafi.github.io/portfolio/ ঠিকানায়। পাতা আসে, কিন্তু কোনো স্টাইল নেই। head-এ লেখা <link rel="stylesheet" href="/style.css">. কারণ কী?
- GitHub Pages CSS ফাইল পরিবেশন করে না
- ব্রাউজারের ক্যাশে পুরোনো CSS রয়ে গেছে
- শুরুর
/-এর কারণে ব্রাউজার ডোমেইনের গোড়ায় CSS খুঁজছে,/portfolio/ফোল্ডারে নয় - link-এ
type="text/css"লেখা হয়নি
যোগাযোগ ফর্মে ঘরের লাল দাগ শুধু পাঠক ঘরটি ছুঁয়ে ভুল রেখে যাওয়ার পর দেখাতে চান, পাতা খোলামাত্র নয়। কোন selector?
:user-invalid:invalid:focus-visible:required
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- আগে পরিকল্পনা: লক্ষ্য, পাঠক, content inventory, সাইটম্যাপ, wireframe — আসল লেখা দিয়ে।
- আগে token, পরে component: oklch রং (contrast মাপা),
clamp()type scale, spacing scale, radius;light-dark()+color-schemeদিয়ে দুই থিম। - কাঠামো: landmark, skip link, একটি h1, পূর্ণ head ও OG; মেনু
popoverদিয়ে, fallback-সহ। - রেসপন্সিভ: কলাম গোনে
auto-fit+minmax(min(100%, …), 1fr), কার্ড সাজে container query-তে, পাতার কাঠামো media query-তে; ছবিতে width-height, srcset, lazy, প্রথমটিতে fetchpriority. - প্রকাশের আগে যাচাই: দুই চেকলিস্ট, Lighthouse, রুব্রিক; তারপর relative path দিয়ে GitHub Pages — আর মোশন শুধু
no-preference-এ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৫ · কী দেখাবে? — HTML-CSS রেন্ডার অনুমানের অনুশীলনপরবর্তী পর্বকোড দেখে ফল অনুমান — পরীক্ষা আর ইন্টারভিউর প্রস্তুতির শুরু।
- HTML ও CSS · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন দক্ষতা কোন পর্বে।
- JavaScript · ক্যাপস্টোন প্রজেক্টপরের ধাপএই সাইটে JavaScript দিয়ে আচরণ যোগ করার পূর্ণ প্রজেক্ট।
- JavaScript · DOM-এর ভিত্তিগভীরে যানথিম মনে রাখা বা মেনু বন্ধ করার মতো ছোট কাজের পেছনের DOM.
- কম্পিউটার ও ICT · ডোমেইন আর হোস্টিংপাশের ট্র্যাকনিজের .com বা .com.bd ডোমেইন কীভাবে কাজ করে।