এই পর্বে যা শিখবেন
- scroll container কী, আর মসৃণ স্ক্রল,
scroll-marginওscroll-paddingদিয়ে লিংকের লক্ষ্য ঠিক জায়গায় আনা scroll-snap-type,scroll-snap-align,scroll-snap-stopদিয়ে পণ্যের carousel আর পুরো-পর্দার গল্প বানানোoverscroll-behaviorদিয়ে scroll chaining থামানো, আর স্ক্রলবার সাজানোর মানসম্মত বনাম পুরোনো পথscroll()ওview()timeline,animation-range, নামওয়ালা timeline — আর এদের সৎ Baseline অবস্থা ও fallback- পুরোনো JavaScript পথ (scroll event, IntersectionObserver) কেন ধীর, parallax-এর ঝুঁকি, আর কিবোর্ড ও reduced motion-বান্ধব স্ক্রল
১ · স্ক্রল কোথায় হয় — scroll container আবার মনে করি
ঢাকার মেট্রো রেলের জানালার কথা ভাবুন। জানালার ফ্রেম স্থির, কিন্তু বাইরের দৃশ্য সরে যাচ্ছে। ওয়েবপাতায় স্ক্রলও ঠিক তাই: একটি স্থির “জানালা”, আর তার পেছনে লম্বা কনটেন্ট সরে যায়। এই জানালাকেই CSS বলে scroll container.
পর্ব ২৩-এ দেখেছেন, কোনো বাক্সে overflow-এর মান auto, scroll বা hidden দিলে বাক্সটি scroll container হয়ে যায়। ভেতরের জিনিস বাক্সের চেয়ে বড় হলে বাক্সের ভেতরেই স্ক্রল হয়, বাকি পাতা নড়ে না। আর পুরো পাতার স্ক্রল? সেটির মালিক হলো রুট element <html> — ব্রাউজার একে বলে viewport-এর scroller.
এই পর্বের প্রায় প্রতিটি property দুই দলের একটিতে পড়ে। ধারকের (scroll container) property: scroll-behavior, scroll-padding, scroll-snap-type, overscroll-behavior, scrollbar-*. ভেতরের জিনিসের property: scroll-margin, scroll-snap-align, scroll-snap-stop. কোনো ইফেক্ট কাজ না করলে প্রথম প্রশ্ন: “আমি কি property-টি ঠিক বাক্সে লিখেছি? সেই বাক্সটি কি আসলেই স্ক্রল হচ্ছে?”
এই কারণেই এই পর্বের লাইভ এডিটরগুলোর ভেতরে নির্দিষ্ট উচ্চতার বাক্স (যেমন height: 240px; overflow-y: auto) রাখা হয়েছে। প্রিভিউ-ফ্রেমের ভেতরে সেই বাক্সই আপনার “ছোট পাতা”। মাউসের চাকা, টাচপ্যাড বা আঙুল দিয়ে বাক্সের ভেতরে স্ক্রল করে প্রতিটি ইফেক্ট নিজে পরীক্ষা করবেন। একটি বাক্স কিবোর্ড দিয়ে স্ক্রল করতে চাইলে Tab চেপে সেটিতে focus আনুন, তারপর তীর-কী (arrow key) বা Space চাপুন।
একটি সূক্ষ্ম কথা: overflow: hidden দিলেও বাক্সটি scroll container থাকে — শুধু ব্যবহারকারী নিজে স্ক্রল করতে পারেন না, কিন্তু JavaScript বা লিংক তাকে স্ক্রল করাতে পারে। আর overflow: clip বাক্সকে scroll container বানায় না। পর্ব ২৪-এ sticky header আটকে না থাকার যে রহস্য দেখেছিলেন, তার অনেকগুলোর মূলে এই পার্থক্য।
২ · মসৃণ স্ক্রল আর লক্ষ্যের সামনে খালি জায়গা
পাতার ওপরের মেনুতে “দাম” লিংকে চাপলেন। ডিফল্টে ব্রাউজার চোখের পলকে লাফিয়ে সেখানে চলে যায়। লম্বা পাতায় এতে পাঠক বুঝতে পারেন না তিনি পাতার কোন অংশে এলেন — মনে হয় হঠাৎ অন্য পাতা খুলে গেল। scroll-behavior: smooth এই লাফটিকে অল্প সময়ের একটি মসৃণ যাত্রায় বদলে দেয়। এটি লিংকের #id, JavaScript-এর scrollIntoView() বা scrollTo() — সব ধরনের “প্রোগ্রাম দিয়ে করা” স্ক্রলে খাটে। ব্যবহারকারী নিজে চাকা ঘোরালে বা আঙুল টানলে এর কোনো প্রভাব নেই।
কিন্তু মসৃণ যাত্রা সবার জন্য আরামের নয়। যাঁদের ভেতরের কানের ভারসাম্যজনিত সমস্যা (vestibular disorder) আছে, বড় দূরত্বের গড়ানো স্ক্রল তাঁদের মাথা ঘোরাতে পারে। তাই পেশাদার নিয়ম হলো, মসৃণ স্ক্রল দিন শুধু তখন, যখন ব্যবহারকারী অপারেটিং সিস্টেমে “অ্যানিমেশন কমান” চালু করেননি — অর্থাৎ prefers-reduced-motion-এর মান no-preference হলে।
দ্বিতীয় সমস্যাটি পর্ব ২৪-এ দেখেছিলেন: sticky বা fixed header থাকলে লিংকে চাপার পর শিরোনামটি header-এর নিচে ঢাকা পড়ে যায়। কারণ ব্রাউজার লক্ষ্যকে স্ক্রল-জানালার একেবারে ওপরের কিনারায় আনে, আর সেখানে তো header বসে আছে। সমাধান দুটি, দুই দিক থেকে:
🪟 scroll-padding — জানালায়
Scroll container-এ বসে। বলে, “কিছু দেখাতে স্ক্রল করার সময় জানালার ওপরের এতটুকু জায়গা বাদ রাখো।” পুরো পাতার জন্য একবার html { scroll-padding-top: 4rem; } লিখলেই সব লিংক ঠিক।
🎯 scroll-margin — লক্ষ্যে
লক্ষ্য element-এ বসে। বলে, “আমাকে দেখানোর সময় আমার চারপাশে এতটুকু ফাঁক রেখো।” নির্দিষ্ট কোনো শিরোনামকে আলাদা দূরত্ব দিতে চাইলে এটি কাজের।
ভেতরের জিনিসের propertyনিচের প্রিভিউতে একটি চায়ের দোকানের ছোট পাতা আছে — একটি নির্দিষ্ট উচ্চতার বাক্স, যার ভেতরে কালো sticky header আর তিনটি অংশ। ওপরের লিংকগুলোতে চাপুন:
<style>
.page { height: 230px; overflow-y: auto; border: 2px solid #94a3b8;
font-family: sans-serif; scroll-padding-top: 52px; }
@media (prefers-reduced-motion: no-preference) {
.page { scroll-behavior: smooth; }
}
.top { position: sticky; top: 0; display: flex; gap: 16px;
padding: 12px; background: #0f172a; }
.top a { color: #fff; }
section { padding: 0 12px 170px; }
h2 { margin: 0; padding-top: 6px; color: #b91c1c; }
</style>
<div class="page" tabindex="0" role="region" aria-label="চায়ের দোকানের পাতা">
<nav class="top"><a href="#menu">মেনু</a> <a href="#price">দাম</a> <a href="#contact">যোগাযোগ</a></nav>
<section id="menu"><h2>মেনু</h2><p>দুধ চা, রং চা, আদা চা, সিঙ্গারা আর পুরি।</p></section>
<section id="price"><h2>দাম</h2><p>দুধ চা ১৫ টাকা, রং চা ১০ টাকা, সিঙ্গারা ১০ টাকা।</p></section>
<section id="contact"><h2>যোগাযোগ</h2><p>নিউ মার্কেটের পাশে, সকাল ৭টা থেকে রাত ১১টা।</p></section>
</div>“দাম” বা “যোগাযোগ” চাপলে বাক্সটি গড়িয়ে গিয়ে লাল শিরোনামটিকে কালো header-এর ঠিক নিচে এনে থামে — ঢাকা পড়ে না, কারণ scroll-padding-top: 52px ওপরের 52px বাদ রেখেছে। আপনার অপারেটিং সিস্টেমে “অ্যানিমেশন কমান” চালু থাকলে গড়ানো হবে না, সরাসরি লাফ দেবে — এটি ইচ্ছাকৃত। চেষ্টা করুন: (১) scroll-padding-top: 52px; মুছে আবার “দাম” চাপুন — শিরোনাম header-এর নিচে লুকিয়ে যাবে। (২) তার বদলে section { scroll-margin-top: 52px; } যোগ করুন — একই ফল, এবার দূরত্বটা লক্ষ্যের গায়ে।
অনেক টিউটোরিয়াল আর AI সোজা লিখে দেয় html { scroll-behavior: smooth; } — কোনো media query ছাড়া। এটি কাজ করে, কিন্তু reduced motion চাওয়া মানুষের কথা ভাবে না। আরেকটি ফাঁদ: পাতায় “Ctrl+F” দিয়ে খোঁজার সময়ও কিছু ব্রাউজার এই মসৃণ গড়ানো ব্যবহার করে, ফলে লম্বা পাতায় খোঁজা ধীর মনে হয়। তাই মসৃণ স্ক্রল সবসময় @media (prefers-reduced-motion: no-preference)-এর ভেতরে লিখুন। scroll-behavior, scroll-padding আর scroll-margin — তিনটিই বহু বছর ধরে সব প্রধান ব্রাউজারে আছে: Baseline Widely available.
৩ · Scroll snap — খাঁজে খাঁজে থামা স্ক্রল
অনলাইন দোকানের অ্যাপে “নতুন পণ্য” সারিটি আঙুলে টানলে পণ্যের কার্ড একটা একটা করে ঠিক জায়গায় এসে থামে — আধখানা কার্ড কেটে থাকে না। আগে এই কাজে Slick বা Swiper-এর মতো JavaScript লাইব্রেরি লাগত: স্ক্রল থামার পর হিসাব করে বাক্সটিকে নিকটতম কার্ডে টেনে নিয়ে যাওয়া। এখন CSS-এর scroll snap সেই কাজটি ব্রাউজারের নিজস্ব স্ক্রল-যন্ত্রের ভেতরেই করে।
পুরোনো দিনের রেডিওর নব ঘোরানোর কথা ভাবুন। কিছু রেডিওর নব যেখানে খুশি থামে — একে বলে মুক্ত ঘোরানো। আবার কিছু নবে খাঁজ কাটা থাকে, ছেড়ে দিলেই “টক” করে নিকটতম খাঁজে বসে যায়। Scroll snap হলো স্ক্রলের নবে খাঁজ কাটা। খাঁজ কোথায় কাটা হবে, সেটি বলে ভেতরের জিনিস; আর নবটি খাঁজ মানবে কি না, কতটা জোর দিয়ে মানবে — সেটি বলে ধারক।
নিচের ডায়াগ্রামে ধাপে ধাপে দেখুন, প্রতিটি লাইন যোগ করলে ব্রাউজারের হিসাব কীভাবে বদলায়:
1.shelf {2 display: flex; overflow-x: auto;3 scroll-snap-type: x mandatory;4 scroll-padding-inline: 16px;5}6.item {7 scroll-snap-align: start;8 scroll-snap-stop: always;9}
| ধারক | |
| খাঁজ | |
| জোরে টানলে |
- শুরু — মুক্ত স্ক্রল।
display: flexকার্ডগুলোকে এক সারিতে রাখে,overflow-x: autoবাক্সকে scroll container বানায়। আঙুল ছাড়লে স্ক্রল ঠিক যেখানে গতি শেষ হয়েছে সেখানেই থামে — প্রায়ই একটি কার্ড আধখানা কেটে। - ধারককে snap container বানানো।
scroll-snap-type: x mandatory-এর প্রথম অংশ অক্ষ বলে:x(আড়াআড়ি),y(খাড়া),block,inlineবাboth. কিন্তু শুধু এটুকুতে কিছুই বদলায় না — নবটি তৈরি, খাঁজ এখনো কাটা হয়নি। - খাঁজ কাটা —
scroll-snap-align. প্রতিটি কার্ড বলে, “আমার বাঁ কিনারাকে (start) জানালার বাঁ কিনারায় মেলানো যায়।”centerদিলে কার্ডের মাঝখান জানালার মাঝখানে মিলবে,endদিলে ডান কিনারা। এখন ছেড়ে দিলেই কার্ড ঠিক জায়গায় বসে। mandatoryবনামproximity.mandatoryমানে স্ক্রল থামলে অবশ্যই কোনো খাঁজে থামবে — carousel আর পুরো-পর্দার স্লাইডে ভালো।proximityমানে খাঁজের কাছাকাছি থামলে তবেই টেনে নেবে, দূরে থাকলে যেখানে খুশি থামতে দেবে — লম্বা লেখা আছে এমন তালিকায় এটিই নিরাপদ।scroll-padding— জানালার ভেতরের মার্জিন। Snapport হলো জানালার সেই অংশ, যার কিনারার সাথে খাঁজ মেলানো হয়।scroll-padding-inline: 16pxজানালাকে দুই পাশে 16px ছোট করে, তাই কার্ড আর কিনারায় সেঁটে থাকে না। অংশ ২-এর sticky header সমস্যার সমাধানও এই একই property.scroll-snap-stop: always— কোনো খাঁজ পেরোনো নিষেধ। ডিফল্টnormal-এ জোরে সোয়াইপ করলে ব্রাউজার তিন-চারটি কার্ড পেরিয়ে যেতে পারে।alwaysদিলে একবারের সোয়াইপে পরের খাঁজেই থামবে। গল্প বা ধাপে ধাপে নির্দেশনায় এটি দরকারি, পণ্যের সারিতে সাধারণত দরকার নেই।
এবার আসল carousel. বাক্সের ভেতরে আড়াআড়ি স্ক্রল করুন — টাচপ্যাডে দুই আঙুলে পাশে টানুন, মাউসে Shift চেপে চাকা ঘোরান, বা Tab দিয়ে সারিটিতে focus এনে ← → চাপুন:
<style>
.shelf { display: flex; gap: 12px; overflow-x: auto; padding: 8px 16px;
scroll-snap-type: x mandatory; scroll-padding-inline: 16px; }
.item { flex: 0 0 70%; height: 130px; border-radius: 12px;
scroll-snap-align: start;
display: grid; place-items: center;
font: 700 20px sans-serif; color: #fff; background: #0e7490; }
.item:nth-child(even) { background: #7c3aed; }
.shelf:focus-visible { outline: 3px solid #f59e0b; }
</style>
<h3>নতুন পণ্য</h3>
<div class="shelf" tabindex="0" role="region" aria-label="নতুন পণ্য, আড়াআড়ি স্ক্রল করুন">
<div class="item">👕 টি-শার্ট ৳৪৫০</div>
<div class="item">👟 কেডস ৳১,২০০</div>
<div class="item">🎒 ব্যাগ ৳৯৫০</div>
<div class="item">⌚ ঘড়ি ৳২,৩০০</div>
<div class="item">🧢 টুপি ৳২৫০</div>
</div>প্রথম কার্ডটি বাক্সের 70% জায়গা নেয়, ডানে পরের কার্ডের এক টুকরো উঁকি দেয় — এটিই ব্যবহারকারীকে বোঝায় “পাশে আরও আছে”। যেভাবেই স্ক্রল করুন, থামার পর একটি কার্ডের বাঁ কিনারা সবসময় 16px ভেতরে এসে বসে। চেষ্টা করুন: (১) mandatory-কে proximity করুন, তারপর অল্প অল্প স্ক্রল করুন — এখন কার্ড মাঝপথেও থামতে পারবে। (২) scroll-snap-align: start-কে center করুন — কার্ড বাক্সের মাঝখানে বসবে, দুই পাশে প্রতিবেশীর টুকরো দেখা যাবে।
পুরো-পর্দার গল্প — খাড়া snap আর scroll-snap-stop
অনেক সংবাদমাধ্যম বড় প্রতিবেদন “স্ক্রল করে পড়ার গল্প” হিসেবে সাজায়: প্রতিটি স্ক্রলে পর্দা-ভরা একটি নতুন দৃশ্য। নিচে রাজশাহীর আমের ঢাকার টেবিলে পৌঁছানোর গল্প — একটি খাড়া snap container, যার প্রতিটি অংশ বাক্সের পুরো উচ্চতা নেয়:
<style>
.story { height: 240px; overflow-y: auto; scroll-snap-type: y mandatory; }
.story section {
height: 100%; scroll-snap-align: start; scroll-snap-stop: always;
display: grid; place-items: center; padding: 0 16px;
font: 700 21px sans-serif; color: #fff; text-align: center;
}
.story section:nth-child(1) { background: #15803d; }
.story section:nth-child(2) { background: #ca8a04; }
.story section:nth-child(3) { background: #c2410c; }
.story section:nth-child(4) { background: #1d4ed8; }
.story:focus-visible { outline: 3px solid #f59e0b; }
</style>
<div class="story" tabindex="0" role="region" aria-label="আমের যাত্রার গল্প">
<section>১ · ভোরে রাজশাহীর বাগানে আম পাড়া</section>
<section>২ · আড়তে বাছাই আর ঝুড়িতে সাজানো</section>
<section>৩ · রাতের ট্রাকে ঢাকার পথে</section>
<section>৪ · কারওয়ান বাজার থেকে আপনার টেবিলে</section>
</div>একটু স্ক্রল করলেই পুরো সবুজ দৃশ্য সরে গিয়ে হলুদ দৃশ্য বাক্স ভরে বসে — আধাআধি অবস্থায় থামে না। জোরে টাচপ্যাড টানলেও scroll-snap-stop: always-এর কারণে প্রতিটি দৃশ্যে একবার থামে। Tab দিয়ে বাক্সে focus এনে ↓ বা Page Down চাপলেও এক দৃশ্য করে এগোয়। চেষ্টা করুন: (১) scroll-snap-stop: always; মুছে খুব জোরে স্ক্রল করুন — এবার এক টানে দুই দৃশ্য পেরিয়ে যেতে পারে। (২) দ্বিতীয় section-এর ভেতরে অনেক লম্বা একটি অনুচ্ছেদ বসিয়ে দেখুন — বাক্সের চেয়ে লম্বা অংশের ভেতরেও ব্রাউজার আপনাকে স্বাভাবিকভাবে পড়তে দেয়।
শেষ পরীক্ষাটির পেছনে spec-এর একটি গুরুত্বপূর্ণ নিয়ম আছে: কোনো snap অংশ snapport-এর চেয়ে বড় হলে ব্রাউজারকে তার ভেতরে মুক্তভাবে স্ক্রল করতে দিতে হয়, নইলে লেখার মাঝের অংশ কখনো পড়াই যেত না। তবু ফোনে লম্বা লেখা আর mandatory একসাথে ঝুঁকিপূর্ণ — পর্দা ছোট হলে প্রায় প্রতিটি অংশই “বাক্সের চেয়ে বড়” হয়ে যায়, আর স্ক্রল অদ্ভুত লাগে। লেখাভরা অংশে proximity বেছে নিন।
৪ · overscroll-behavior — স্ক্রল যেন পাশের বাক্সে না গড়ায়
bKash বা অনলাইন দোকানের সাইটে সাহায্যের চ্যাট বক্স খুলে পুরোনো মেসেজ পড়তে ওপরে স্ক্রল করছেন। চ্যাটের একেবারে শুরুতে পৌঁছে আরও একটু ঘোরালেন — আর হঠাৎ পেছনের পুরো পাতাটা নড়ে উঠল! একে বলে scroll chaining: একটি scroll container তার সীমানায় পৌঁছালে বাড়তি স্ক্রল তার বাইরের scroll container-এর হাতে চলে যায়, শিকলের মতো।
সাধারণ পাতায় এটি সুবিধাজনক — ছোট একটি টেবিল-বাক্সের শেষে পৌঁছে আপনি পাতা নিচে নামাতে চান। কিন্তু চ্যাট বক্স, modal, পাশের মেনু বা ম্যাপে এটি বিরক্তিকর। overscroll-behavior ধারককে বলে দেয়, সীমানায় পৌঁছে কী করবে:
| মান | বাইরের বাক্সে গড়ায়? | নিজের সীমানার ইফেক্ট (ঝাঁকুনি, আলো) | কখন |
|---|---|---|---|
auto (ডিফল্ট) | হ্যাঁ | হ্যাঁ | সাধারণ বাক্স |
contain | না | হ্যাঁ | চ্যাট, modal, পাশের মেনু |
none | না | না | ম্যাপ, গেম, নিজস্ব টানার ইফেক্ট |
নিচে একটি “পাতা” (বাইরের বাক্স) আর তার ওপরে পাশাপাশি দুটি চ্যাট বক্স। দুটি চ্যাটেই মাউস রেখে স্ক্রল করে একেবারে নিচে নামুন, তারপর আরও ঘোরাতে থাকুন:
<style>
.page { height: 270px; overflow-y: auto; border: 2px solid #94a3b8;
padding: 10px; font-family: sans-serif; }
.chats { display: flex; gap: 10px; }
.chat { flex: 1; height: 120px; overflow-y: auto; padding: 6px;
border: 2px solid #b91c1c; }
.chat p { margin: 0 0 6px; padding: 4px 8px; border-radius: 8px; background: #e0f2fe; }
.safe { overscroll-behavior: contain; border-color: #15803d; }
.rest { height: 480px; margin-top: 10px; padding: 8px;
background: linear-gradient(#f8fafc, #94a3b8); }
</style>
<div class="page" tabindex="0" role="region" aria-label="পুরো পাতা">
<div class="chats">
<div class="chat" tabindex="0" role="region" aria-label="চ্যাট, contain ছাড়া">
<b>সাধারণ</b><p>হ্যালো!</p><p>অর্ডার কোথায়?</p><p>দেখছি…</p><p>পথে আছে</p><p>ধন্যবাদ</p>
</div>
<div class="chat safe" tabindex="0" role="region" aria-label="চ্যাট, contain সহ">
<b>contain</b><p>হ্যালো!</p><p>অর্ডার কোথায়?</p><p>দেখছি…</p><p>পথে আছে</p><p>ধন্যবাদ</p>
</div>
</div>
<div class="rest">বাকি পাতা — এখানে নিচে আরও অনেক কিছু…</div>
</div>লাল বর্ডারের “সাধারণ” চ্যাটের শেষে পৌঁছে আরও ঘোরালে বাইরের ধূসর পাতাটি নিচে নামতে শুরু করে — এটিই scroll chaining. সবুজ বর্ডারের “contain” চ্যাটে শেষে পৌঁছালে স্ক্রল সেখানেই থেমে থাকে, পাতা নড়ে না। চেষ্টা করুন: (১) .safe-এর contain-কে auto করুন — দুটি চ্যাট একই রকম আচরণ করবে। (২) .page-এ overscroll-behavior: contain; যোগ করুন — এবার বাইরের বাক্সও প্রিভিউর বাকি অংশে স্ক্রল গড়াতে দেবে না।
Pull-to-refresh আর পুরো পাতার overscroll
ফোনের Chrome-এ পাতার একেবারে ওপরে থেকে আঙুল নিচে টানলে একটি ঘোরানো চিহ্ন এসে পাতা নতুন করে লোড হয় — একে বলে pull-to-refresh. চ্যাট অ্যাপ বা আঁকার অ্যাপের মতো ওয়েব অ্যাপে এটি বিপদ: ব্যবহারকারী আঁকতে গিয়ে নিচে টানলেন, আর পাতা রিলোড হয়ে সব কাজ হারিয়ে গেল। পুরো পাতার overscroll-এর মালিক রুট element, তাই নিয়মটি সেখানে লিখতে হয়:
/* আঁকার অ্যাপ, ম্যাপ বা নিজের "টেনে রিফ্রেশ" বানানো অ্যাপে */
html {
overscroll-behavior-y: contain; /* বা none */
}overscroll-behavior সব প্রধান ব্রাউজারে আছে — Baseline Widely available. তবে খেয়াল রাখুন: কোন ব্রাউজার আদৌ pull-to-refresh বা সীমানার ঝাঁকুনি (bounce) দেখায়, সেটি ব্রাউজার ও ডিভাইসভেদে আলাদা। সাধারণ তথ্যভিত্তিক পাতায় এটি বন্ধ করবেন না — মানুষ রিফ্রেশের এই অভ্যাসে অভ্যস্ত।
৫ · স্ক্রলবার সাজানো — মানসম্মত পথ বনাম পুরোনো কৌশল
সাইটের রং সবুজ, অথচ পাশের ছোট বাক্সের স্ক্রলবার ধূসর আর মোটা — ডিজাইনাররা প্রায়ই এটি বদলাতে চান। এর দুটি পথ আছে, আর দুটির ইতিহাস ও মর্যাদা আলাদা:
✅ মানসম্মত: scrollbar-width আর scrollbar-color
scrollbar-width: auto, thin বা none. scrollbar-color: দুটি রং — প্রথমটি টেনে ধরার অংশের (thumb), দ্বিতীয়টি পথের (track). আকার-আকৃতির খুঁটিনাটি ব্রাউজার ঠিক করে, তাই স্ক্রলবার ব্যবহারযোগ্য থাকে।
⚠️ অমানক: ::-webkit-scrollbar
WebKit/Chromium-এর নিজস্ব pseudo-element-গুচ্ছ (::-webkit-scrollbar, -thumb, -track…). প্রস্থ, গোল কোণা — অনেক কিছু বদলানো যায়, কিন্তু এটি কোনো মানদণ্ডের অংশ নয়, আর Firefox কখনো সমর্থন করেনি।
.box {
height: 70px; overflow-y: auto; margin-bottom: 12px; padding: 8px;
border: 2px solid #94a3b8; font-family: sans-serif;
scrollbar-gutter: stable; /* স্ক্রলবারের জায়গা আগেই রাখা */
}
/* মানসম্মত পথ */
.std { scrollbar-width: thin; scrollbar-color: #0e7490 #e0f2fe; }
/* পুরোনো, অমানক পথ */
.old::-webkit-scrollbar { width: 14px; }
.old::-webkit-scrollbar-track { background: #fef3c7; }
.old::-webkit-scrollbar-thumb { background: #b45309; border-radius: 7px;
border: 3px solid #fef3c7; }Edge বা Chrome-এ ওপরের বাক্সে সরু নীল thumb আর হালকা নীল track, নিচের বাক্সে মোটা, গোল কোণার বাদামি thumb দেখবেন। Firefox-এ নিচের বাক্সটি সাধারণ স্ক্রলবারই দেখাবে — সে ::-webkit-scrollbar চেনে না। (কিছু সিস্টেমে স্ক্রলবার শুধু স্ক্রল করার সময় ভেসে ওঠে।) চেষ্টা করুন: (১) .old-এ scrollbar-color: #b45309 #fef3c7; যোগ করুন — নতুন Chromium-এ গোল কোণার নকশা মুছে যাবে, কারণ মানসম্মত property থাকলে সে -webkit- নিয়ম উপেক্ষা করে। (২) .std-এ thin-কে none করুন — স্ক্রলবার লুকাবে, কিন্তু চাকা বা কিবোর্ডে স্ক্রল তখনো চলবে।
সাপোর্টের সৎ ছবি: Firefox এই দুটি মানসম্মত property অনেক বছর আগে চালু করে; Chromium যোগ দেয় ২০২৪ সালের শুরুতে। Safari প্রথমে scrollbar-width আনে (২০২৪-এর শেষে, ফলে এটি Baseline Newly available), আর scrollbar-color Safari-তে আসছে তারও পরে — কাজে লাগানোর আগে Can I use-এ বর্তমান অবস্থা দেখে নিন। সুখবর হলো, যে ব্রাউজার চেনে না, সে শুধু সাধারণ স্ক্রলবার দেখায় — কিছুই ভাঙে না।
scrollbar-gutter: stable পর্ব ২৩-এ দেখেছেন: স্ক্রলবারের জায়গা আগেই খালি রাখে, তাই লেখা বাড়তে বাড়তে হঠাৎ স্ক্রলবার এলে কনটেন্ট পাশে লাফায় না। সাজানো স্ক্রলবারের সাথে এটি প্রায় সবসময় জোড়ায় লিখুন।
scrollbar-width: none দিয়ে স্ক্রলবার লুকানোর আগে দুবার ভাবুন। স্ক্রলবার শুধু নিয়ন্ত্রণ নয়, একটি সংকেতও: “এখানে আরও লেখা আছে, আর আপনি এখন এতটুকু পড়েছেন।” লুকালে মাউস-নির্ভর ব্যবহারকারী বুঝতেই পারেন না বাক্সটি স্ক্রল হয়। লুকাতেই হলে carousel-এর মতো জায়গায় “পরের পণ্য উঁকি দেওয়া” বা তীর-বোতামের মতো অন্য সংকেত রাখুন। আর অতিরিক্ত সরু thumb কাঁপা হাতের মানুষের পক্ষে ধরা কঠিন।
৬ · Scroll-driven animation — ঘড়ির বদলে স্ক্রল
পর্ব ৩৯-এ দেখেছেন, @keyframes দিয়ে লেখা animation একটি ঘড়ি ধরে চলে: 2s দিলে দুই সেকেন্ডে শুরু থেকে শেষ। এই ঘড়িকে বলে timeline — ডিফল্টে সেটি পাতার সময়রেখা (document timeline). Scroll-driven animation-এর মূল চিন্তা এক লাইনের: ঘড়িটা খুলে ফেলে তার জায়গায় স্ক্রলবার বসিয়ে দিন। স্ক্রল ০% হলে animation-এর শুরু, স্ক্রল ১০০% হলে শেষ; পাঠক উল্টো দিকে স্ক্রল করলে animation-ও উল্টো দিকে চলে।
সিনেমা হলে ছবি চলে প্রজেক্টরের ঘড়ি ধরে — আপনি চুপ করে বসে থাকলেও গল্প এগোয়। কিন্তু হাতে একটি ফ্লিপবুক (পাতা উল্টালে ছবি নড়ে এমন খাতা) নিলে গল্প এগোয় শুধু আপনি যত দ্রুত পাতা উল্টান, তত দ্রুত — থামলে থামে, পেছনে উল্টালে পেছনে যায়। সাধারণ animation হলো সিনেমা, scroll-driven animation হলো ফ্লিপবুক।
CSS দুই রকম স্ক্রল-ঘড়ি দেয়:
- Scroll progress timeline —
scroll(): একটি scroll container কতটা স্ক্রল হয়েছে, ০% থেকে ১০০%. পড়ার অগ্রগতির দাগের (reading progress bar) জন্য আদর্শ। - View progress timeline —
view(): একটি নির্দিষ্ট element স্ক্রল-জানালায় কতটা দেখা যাচ্ছে — ঢোকা থেকে বেরিয়ে যাওয়া পর্যন্ত। কার্ড বা ছবি পর্দায় ঢোকার সময় ফুটে ওঠার (fade-in) জন্য আদর্শ।
প্রথমে scroll()-এর ভেতরের হিসাবটি ধাপে ধাপে দেখুন। ধরুন একটি প্রবন্ধ-বাক্সে মোট স্ক্রল করার জায়গা 600px:
1@keyframes grow {2 from { transform: scaleX(0); }3 to { transform: scaleX(1); }4}5.bar {6 animation: grow linear both;7 animation-timeline: scroll(nearest block);8}
| timeline | |
| scrollTop | |
| অগ্রগতি | |
| scaleX |
- একই পুরোনো
@keyframes. নতুন কিছু শিখতে হয় না — দাগটি প্রস্থে ০ থেকে পূর্ণ হবে।width-এর বদলেtransform: scaleX()বেছে নেওয়া হয়েছে, কারণ transform বদলালে লেআউট নতুন করে হিসাব করতে হয় না; ব্রাউজার শুধু আঁকা ছবিটা টেনে বড় করে। - সময়কাল ছাড়া shorthand.
animation: grow linear both-এ কোনো2sনেই — স্ক্রল-ঘড়িতে সেকেন্ডের অর্থ নেই, সময়কাল থাকেauto.linearদিলে স্ক্রলের সাথে সমান তালে বাড়বে;bothদিলে শুরুর আগে আর শেষের পরেও প্রথম ও শেষ অবস্থা ধরে রাখবে। - ঘড়ি বদল —
animation-timeline.scroll(nearest block)মানে “আমার নিকটতম পূর্বপুরুষ scroll container-এর block অক্ষের (সাধারণত খাড়া) স্ক্রল ধরো।” অন্য বিকল্প:root(পুরো পাতা) আরself(element নিজে)। শুরুতেscrollTop0, তাই অগ্রগতি 0%, দাগ অদৃশ্য। - পাঠক নিচে নামছেন। 600px-এর মধ্যে 150px স্ক্রল = 25%. ব্রাউজার keyframe-এর 25% বিন্দুর মান বসায়:
scaleX(0.25). কোনো JavaScript নেই, কোনো scroll event নেই — হিসাবটা ব্রাউজারের স্ক্রল-যন্ত্রের সাথেই হয়। - প্রবন্ধের শেষ। 600px / 600px = 100%, দাগ পুরো চওড়া। এখানে একটি জরুরি নিয়ম:
animation-timelineসবসময়animationshorthand-এর পরে লিখুন। shorthand নিজের ভেতরেanimation-timeline-কে ডিফল্টে (auto) ফিরিয়ে দেয়, তাই আগে লিখলে মুছে যায়। - পাঠক আবার ওপরে উঠলেন। 300px = 50%, দাগ অর্ধেকে ফিরে এলো। ঘড়ির animation কখনো পেছনে যায় না, কিন্তু স্ক্রল-ঘড়ি দুই দিকেই চলে — এটিই ফ্লিপবুক।
এবার চালিয়ে দেখুন। সবুজ দাগটি প্রবন্ধ-বাক্সের ভেতরে sticky — তাই সে এই বাক্সেরই নিকটতম scroller খুঁজে পায়:
<style>
.article { height: 150px; overflow-y: auto; border: 2px solid #94a3b8;
font-family: sans-serif; line-height: 1.7; }
.article p { margin: 12px; }
.bar { position: sticky; top: 0; height: 8px; background: #16a34a;
transform-origin: 0 50%; display: none; }
.note { color: #b91c1c; }
@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@supports (animation-timeline: scroll()) {
.bar { display: block; animation: grow linear both;
animation-timeline: scroll(nearest block); }
.note { display: none; }
}
</style>
<p class="note">আপনার ব্রাউজার scroll-driven animation চেনে না — লেখা পড়া যাবে, শুধু অগ্রগতির দাগ দেখাবে না।</p>
<div class="article" tabindex="0" role="region" aria-label="প্রবন্ধ: পদ্মা নদী">
<div class="bar"></div>
<p>পদ্মা হিমালয় থেকে নেমে আসা গঙ্গার মূল ধারা, যা বাংলাদেশে ঢুকে পদ্মা নাম নেয়।</p>
<p>গোয়ালন্দের কাছে যমুনা এসে মেলে, আর চাঁদপুরের কাছে মেঘনার সাথে মিলে সে সাগরের দিকে যায়।</p>
<p>বর্ষায় পদ্মা কয়েক কিলোমিটার চওড়া হয়ে যায়, আর তার ভাঙনে প্রতি বছর বহু গ্রাম নদীতে হারায়।</p>
<p>ইলিশ, পলিমাটি আর নৌকার গান — পদ্মা এ দেশের অর্থনীতি আর সাহিত্য দুটোরই অংশ।</p>
<p>রাজশাহী, পাবনা, কুষ্টিয়া, ফরিদপুর আর শরীয়তপুরের মতো বহু জেলা পদ্মার তীরে।</p>
<p>২০২২ সালে পদ্মা সেতু খুলে দেওয়ায় দক্ষিণ-পশ্চিমের জেলাগুলোর সাথে ঢাকার যাতায়াত অনেক সহজ হয়েছে।</p>
</div>Edge বা Chrome-এ শুরুতে দাগ অদৃশ্য; প্রবন্ধ-বাক্সে নিচে নামতে থাকলে সবুজ দাগ বাঁ থেকে ডানে বাড়ে, আর শেষ অনুচ্ছেদে পৌঁছালে পুরো চওড়া হয়। ওপরে উঠলে দাগ ছোট হয়ে যায়। যে ব্রাউজার এই ফিচার চেনে না, সেখানে লাল নোট দেখাবে আর দাগ লুকানো থাকবে — প্রবন্ধ পড়তে কোনো অসুবিধা হবে না। চেষ্টা করুন: (১) animation-timeline লাইনটি কেটে animation: grow …-এর আগে বসান — দাগ আর স্ক্রল মানবে না (সময়কাল auto হওয়ায় সাধারণ ঘড়িতে চোখের পলকে শেষ হয়ে পূর্ণ দাগ দেখাবে)। (২) linear-কে ease-in করুন — শুরুতে দাগ ধীরে, শেষে দ্রুত বাড়বে।
view() আর animation-range — কোন অংশে animation চলবে
view()-এর ঘড়ি একটি element-এর পুরো “পর্দা-যাত্রা” মাপে: তার ওপরের কিনারা জানালার নিচ দিয়ে ঢোকার মুহূর্ত (০%) থেকে তার নিচের কিনারা জানালার ওপর দিয়ে বেরিয়ে যাওয়া (১০০%) পর্যন্ত। কিন্তু fade-in-এর জন্য আমরা পুরো যাত্রা চাই না — শুধু ঢোকার অংশটুকু। সেটাই বলে দেয় animation-range. নিচের ডায়াগ্রামে একটি কার্ডের যাত্রা দেখুন:
- cover
০% → ১০০%পুরো যাত্রা - entry
নিচ দিয়ে ঢোকাআংশিক দেখা যায় - contain
পুরোটা ভেতরেসম্পূর্ণ দেখা যায় - exit
ওপর দিয়ে বেরোনোআবার আংশিক - animation-range
entry 0% entry 100%শুধু ঢোকার সময়
cover— পুরো যাত্রা। কার্ডের ওপরের কিনারা জানালার নিচের কিনারা ছুঁলো:cover 0%. কার্ডের নিচের কিনারা জানালার ওপরের কিনারা পেরিয়ে গেল:cover 100%. কোনো range না দিলেview()animation এই পুরো পথেই ছড়িয়ে চলে — অর্থাৎ কার্ড পর্দার মাঝখানে থাকার সময়ও animation অর্ধেক অবস্থায় থাকবে।entry— ঢোকার পর্ব। কার্ড নিচ দিয়ে উঁকি দিতে শুরু করল (entry 0%) থেকে পুরো কার্ড জানালার ভেতরে চলে এলো (entry 100%). Fade-in-এর জন্য ঠিক এই পর্বটিই দরকার।contain— পুরোটা দেখা যায়। কার্ড জানালার চেয়ে ছোট হলে এটি সেই সময়, যখন কার্ড সম্পূর্ণ ভেতরে। কার্ড জানালার চেয়ে বড় হলে উল্টো — কার্ড পুরো জানালা ঢেকে রাখার সময়। এই পর্বে animation সাধারণত স্থির থাকে।exit— বেরোনোর পর্ব। কার্ডের ওপরের কিনারা জানালার ওপর দিয়ে বেরোতে শুরু করল (exit 0%) থেকে পুরো কার্ড বেরিয়ে গেল (exit 100%). “বেরোনোর সময় ঝাপসা হয়ে যাওয়া” ইফেক্ট এখানে বসে। এ ছাড়াentry-crossingআরexit-crossingনামে আরও দুটি পর্ব আছে, যা প্রথমবার দরকার হয় না।- পর্ব বেছে নেওয়া।
animation-range: entry 0% entry 100%(সংক্ষেপেanimation-range: entry) বলে, “animation-এর ০% বসাও entry-র শুরুতে, ১০০% বসাও entry-র শেষে।”bothfill থাকায় তার আগে কার্ড অদৃশ্য, তার পরে পুরো দৃশ্যমান। শতাংশের বদলেpx-ও চলে:entry 20% cover 50%-এর মতো দুই পর্ব মিশিয়েও লেখা যায়।
<style>
.feed { height: 220px; overflow-y: auto; padding: 10px;
border: 2px solid #94a3b8; font-family: sans-serif; }
.card { height: 80px; margin-bottom: 14px; border-radius: 12px;
display: grid; place-items: center; font-size: 18px; background: #fde68a; }
@keyframes reveal {
from { opacity: 0; transform: translateY(40px) scale(0.9); }
to { opacity: 1; transform: none; }
}
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.card { animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%; }
}
}
</style>
<div class="feed" tabindex="0" role="region" aria-label="খবরের তালিকা">
<div class="card">🏏 ক্রিকেট</div><div class="card">🌦️ আবহাওয়া</div>
<div class="card">🚇 মেট্রো রেল</div><div class="card">🌾 কৃষি</div>
<div class="card">📚 শিক্ষা</div><div class="card">💻 প্রযুক্তি</div>
<div class="card">🎭 সংস্কৃতি</div><div class="card">🏥 স্বাস্থ্য</div>
</div>শুরুতে ওপরের দুটি কার্ড পুরো দেখা যায়, তৃতীয়টি আধা-স্বচ্ছ আর একটু নিচে সরানো — সে এখনো “ঢুকছে”। নিচে স্ক্রল করলে প্রতিটি কার্ড নিচ থেকে উঠে এসে স্পষ্ট হয়; ওপরে উঠলে আবার ঝাপসা হয়ে নেমে যায়। ফিচারটি না চেনা ব্রাউজারে, বা reduced motion চাইলে, সব কার্ড শুরু থেকেই স্বাভাবিক দেখায় — কারণ animation শুধু @supports আর no-preference-এর ভেতরে। চেষ্টা করুন: (১) animation-range লাইনটি মুছুন — এখন animation পুরো cover জুড়ে চলবে, তাই পর্দার মাঝখানের কার্ডও অর্ধেক ঝাপসা থাকবে। (২) entry 0% entry 100%-কে entry 0% cover 30% করুন — ফুটে ওঠা আরও ধীরে, দীর্ঘ পথে ছড়াবে।
সবচেয়ে বড় ভুলটি হলো, CSS-এর সাধারণ অংশে .card { opacity: 0; } লিখে রাখা আর animation দিয়ে 1-এ আনা। যে ব্রাউজার animation-timeline চেনে না, সেখানে কার্ডগুলো চিরকাল অদৃশ্য থেকে যাবে! নিয়ম: স্বাভাবিক অবস্থায় কনটেন্ট সবসময় দৃশ্যমান রাখুন; লুকানো আর ফুটিয়ে তোলার পুরো কাজটি @supports-এর ভেতরে, keyframe-এর from-এ রাখুন। এটিই progressive enhancement.
নামওয়ালা timeline — দাগটি যখন বাক্সের বাইরে
scroll(nearest) শুধু নিজের পূর্বপুরুষ scroller-কে চেনে। কিন্তু ধরুন অগ্রগতির দাগটি প্রবন্ধ-বাক্সের বাইরে, পাতার ওপরের header-এ বসবে। তখন scroller-কে একটি নাম দিতে হয়, আর দাগকে বলতে হয় “ওই নামের ঘড়ি ধরো”। দুটি যদি ভাই-বোন বা দূর সম্পর্কের আত্মীয় হয়, তাহলে তাদের সাধারণ পূর্বপুরুষে timeline-scope দিয়ে নামটি “দৃশ্যমান” করতে হয়:
.layout { timeline-scope: --article; } /* সাধারণ পূর্বপুরুষ */
.article { overflow-y: auto; scroll-timeline: --article block; }
.bar { animation: grow linear both;
animation-timeline: --article; } /* নাম ধরে ঘড়ি */
/* একইভাবে element-এর পর্দা-যাত্রারও নাম দেওয়া যায় */
.hero-img { view-timeline: --hero; }
.caption { animation: fade linear both; animation-timeline: --hero; }এখানে নাম দুই হাইফেন দিয়ে শুরু, custom property-র মতো দেখতে হলেও এটি কোনো variable নয় — একটি named timeline-এর পরিচয়। scroll-timeline হলো scroll()-এর নামওয়ালা রূপ, view-timeline হলো view()-এর।
সাপোর্ট — সৎভাবে
| ফিচার | Baseline অবস্থা (২০২৫–২৬) | মন্তব্য |
|---|---|---|
scroll-behavior, scroll-margin, scroll-padding | Widely available | নিশ্চিন্তে ব্যবহার করুন |
scroll-snap-* | Widely available | carousel, স্লাইড, গল্প |
overscroll-behavior | Widely available | চ্যাট, modal, অ্যাপ |
scrollbar-gutter | Newly available | না চিনলে শুধু লাফ থাকে |
scrollbar-width / scrollbar-color | width: Newly available; color: Safari-তে সবার পরে — Can I use দেখে নিন | না চিনলে সাধারণ স্ক্রলবার |
animation-timeline, scroll(), view(), animation-range | not yet Baseline | Chromium (২০২৩ থেকে), Safari 26; Firefox-এ পরীক্ষামূলক — @supports জরুরি |
৭ · পুরোনো JavaScript পথ — আর CSS কেন মসৃণ
scroll-driven animation আসার আগে প্রায় দুই দশক ধরে এই কাজগুলো হতো JavaScript-এ। প্রথম যুগের কৌশল ছিল scroll event শোনা: প্রতিবার পাতা একটু নড়লেই একটি ফাংশন চালিয়ে হিসাব করা, তারপর স্টাইল বদলানো।
// পুরোনো কৌশল — শেখার জন্য দেখুন, নতুন কাজে এড়িয়ে চলুন
window.addEventListener('scroll', function () {
const max = document.documentElement.scrollHeight - innerHeight;
bar.style.width = (scrollY / max * 100) + '%'; // প্রতি নড়াচড়ায় লেআউট
});এতে তিনটি সমস্যা। (১) JavaScript চলে ব্রাউজারের main thread-এ — যে একই “কর্মী” ক্লিক সামলায়, লেআউট হিসাব করে, অন্য স্ক্রিপ্ট চালায়। সে ব্যস্ত থাকলে দাগ পিছিয়ে পড়ে, আর পাতা ঝাঁকুনি দিয়ে চলে — একে বলে jank. (২) আধুনিক ব্রাউজার স্ক্রল করে আলাদা একটি thread-এ, তাই scroll event আসে স্ক্রলের পরে — দাগ সবসময় এক ফ্রেম পিছিয়ে থাকে। (৩) width বদলালে প্রতিবার লেআউট নতুন করে হয়।
দ্বিতীয় যুগে এলো IntersectionObserver — ব্রাউজারকে বলে রাখা, “এই element জানালায় ঢুকলে বা বেরোলে আমাকে জানিয়ো।” প্রতি পিক্সেলে নয়, শুধু সীমানা পেরোনোর মুহূর্তে খবর আসে, তাই এটি অনেক দক্ষ। নিচে এর ছোট্ট একটি উদাহরণ — অর্ধেকের বেশি দেখা গেলে কার্ডে seen class বসে:
<style>
.feed { height: 180px; overflow-y: auto; padding: 10px;
border: 2px solid #94a3b8; font-family: sans-serif; }
.card { height: 70px; margin-bottom: 12px; border-radius: 10px; display: grid;
place-items: center; background: #bfdbfe; opacity: 0.2; transition: opacity 0.5s; }
.card.seen { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .card { transition: none; } }
</style>
<div class="feed" id="feed" tabindex="0" role="region" aria-label="কার্ডের তালিকা">
<div class="card">ঢাকা</div><div class="card">চট্টগ্রাম</div><div class="card">খুলনা</div>
<div class="card">রাজশাহী</div><div class="card">সিলেট</div><div class="card">বরিশাল</div>
</div>
<script>
const feed = document.getElementById('feed');
const io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
e.target.classList.add('seen');
io.unobserve(e.target); // একবার দেখা হলেই যথেষ্ট
console.log('দেখা গেল: ' + e.target.textContent);
});
}, { root: feed, threshold: 0 });
feed.querySelectorAll('.card').forEach(function (c) { io.observe(c); });
</script>শুরুতেই ঢাকা, চট্টগ্রাম আর নিচে সামান্য উঁকি দেওয়া খুলনা স্পষ্ট হয়ে ওঠে, আর নিচে “দেখা গেল: ঢাকা” ধরনের তিনটি লাইন আসে। রাজশাহী থেকে বাকি কার্ডগুলো ঝাপসা; স্ক্রল করে কোনোটির এক টুকরো জানালায় ঢুকলেই সে ফুটে ওঠে, আর নতুন লাইন যোগ হয়। লক্ষ করুন, এখানে animation ঘড়ি ধরে (transition: 0.5s) চলে, স্ক্রল ধরে নয় — একবার ফুটে উঠলে উল্টো স্ক্রলে আর ঝাপসা হয় না। আর JavaScript না চললে সব কার্ড চিরকাল ঝাপসা থাকবে — এটি এই পুরোনো কৌশলের একটি দুর্বলতা। চেষ্টা করুন: (১) threshold: 0-কে 1 করুন — এবার কার্ড পুরোপুরি জানালার ভেতরে এলে তবেই ফুটবে। (২) forEach-এর ভেতরের চারটি লাইন মুছে শুধু e.target.classList.toggle('seen', e.isIntersecting); লিখুন — এবার বেরিয়ে গেলে আবার ঝাপসা হবে।
তাহলে CSS কেন আরও মসৃণ? animation-timeline দিয়ে transform আর opacity animate করলে ব্রাউজার পুরো হিসাবটা স্ক্রলের সাথে একই জায়গায় — compositing-এর ধাপে, প্রায়ই আলাদা thread-এ — করতে পারে। main thread ভারী JavaScript-এ আটকে থাকলেও দাগ আঙুলের সাথে তাল মিলিয়ে চলে। IntersectionObserver এখনো খুব কাজের: lazy loading, “পাঠক এই অংশ দেখেছেন” হিসাব, অসীম স্ক্রলের পরের পাতা আনা — যেখানে animation নয়, একটি সিদ্ধান্ত দরকার। এটি বহু বছর ধরে সব ব্রাউজারে আছে — Baseline Widely available. JavaScript-এর event নিয়ে আরও জানতে দেখুন JavaScript কোর্সের event-এর পর্ব.
আরও গভীরে: আধুনিক ব্রাউজার স্ক্রলকে main thread থেকে আলাদা করে “async scrolling” বা threaded scrolling-এ চালায় — আঙুল নড়লে compositor আগে থেকে আঁকা স্তরগুলো (layer) সরিয়ে দেয়, JavaScript-এর অপেক্ষা করে না। এ কারণেই scroll event দিয়ে কোনো element-কে স্ক্রলের সাথে “আঠা দিয়ে লাগানো” চেষ্টায় কাঁপুনি দেখা যায়; Firefox তো DevTools console-এ এমন পাতার জন্য “scroll-linked positioning effect” নামে সতর্কবার্তাও দেখায়। Scroll-driven animation-এর spec ঠিক এই ফাঁক পূরণের জন্য নকশা করা: timeline-এর মান স্ক্রল-অবস্থান থেকে সরাসরি আসে, তাই compositor নিজেই keyframe-এর মান বসাতে পারে। তবে width বা top-এর মতো লেআউট-বদলানো property animate করলে এই সুবিধা হারায় — তখন আবার main thread লাগে।
৮ · Parallax, carousel আর সবার জন্য স্ক্রল
Parallax হলো সেই ইফেক্ট, যেখানে পেছনের ছবি সামনের লেখার চেয়ে ধীরে সরে — ট্রেনের জানালা দিয়ে দূরের পাহাড় ধীরে আর কাছের গাছ দ্রুত সরে যাওয়ার মতো। এতে গভীরতার অনুভূতি আসে। scroll-driven animation দিয়ে এটি এখন খুব সহজ:
@keyframes drift { from { transform: translateY(-15%); } to { transform: translateY(15%); } }
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.hero-bg { animation: drift linear both; animation-timeline: view(); }
}
}কিন্তু সহজ বলেই বেশি ব্যবহারের ঝুঁকি। পর্দার বড় অংশ জুড়ে বিভিন্ন গতিতে জিনিস সরলে vestibular disorder, মাইগ্রেন বা মোশন সিকনেস আছে এমন মানুষের মাথা ঘোরা, বমিভাব হতে পারে। WCAG 2.2-এর একটি নির্দেশিকা (2.3.3 Animation from Interactions, স্তর AAA) বলে: ব্যবহারকারীর ক্রিয়া থেকে শুরু হওয়া অপ্রয়োজনীয় নড়াচড়া বন্ধ করার উপায় থাকতে হবে। তাই parallax ছোট রাখুন (১০–২০%-এর মধ্যে), পুরো পাতায় নয়, একটি-দুটি জায়গায় দিন, আর সবসময় reduced motion-এর ভেতরে রাখুন। পর্ব ৪৫-এ reduced motion নিয়ে আরও বিস্তারিত থাকবে।
Snap carousel কিবোর্ড আর screen reader-এ
একটি স্ক্রল-বাক্স মাউস ও আঙুলে চলে ঠিকই, কিন্তু যিনি শুধু কিবোর্ড ব্যবহার করেন, তিনি কীভাবে স্ক্রল করবেন? কিবোর্ডে স্ক্রল করতে বাক্সটিতে আগে focus আনতে হয়। ভেতরে লিংক বা বোতাম থাকলে Tab চাপলে সেগুলোয় focus যায়, আর ব্রাউজার নিজেই বাক্সটি স্ক্রল করে focus পাওয়া জিনিস দেখায়। কিন্তু ভেতরে শুধু লেখা বা ছবি থাকলে? তখন বাক্সটিকেই focus পাওয়ার যোগ্য করতে হয়। এই পর্বের প্রতিটি এডিটরে তাই এই তিনটি জিনিস দেখেছেন:
<div class="shelf" tabindex="0" role="region" aria-label="নতুন পণ্য">
…কার্ডগুলো…
</div>tabindex="0"— বাক্সটি Tab-এর ক্রমে আসে, focus পেলে তীর-কী দিয়ে স্ক্রল করা যায়। কিছু ব্রাউজার আজকাল কনটেন্ট-ভরা scroller-কে নিজে থেকেই focusable করে, কিন্তু সবাই নয় — তাই নিজে লিখে দেওয়াই নিরাপদ।role="region"+aria-label— focus পেলে screen reader বলে “নতুন পণ্য, অঞ্চল”। নাম ছাড়া focus পাওয়া একটি নামহীন বাক্স শ্রোতার কাছে অর্থহীন। ARIA-র প্রথম নিয়ম মনে রাখুন:<section>-এর মতো উপযুক্ত HTML থাকলে আগে সেটি ব্যবহার করুন; এখানে একটি সাধারণ<div>-কে নাম দিচ্ছি বলে role লাগছে।:focus-visibleoutline — focus কোথায় আছে, চোখে দেখা যাওয়া চাই।
আরও কয়েকটি নিয়ম: carousel নিজে নিজে ঘুরলে (auto-play) থামানোর বোতাম দিন — WCAG 2.2.2 (Pause, Stop, Hide) পাঁচ সেকেন্ডের বেশি চলা নড়াচড়ার জন্য এটি দাবি করে। স্ক্রলের স্বাভাবিক আচরণ ছিনিয়ে নেবেন না — চাকা একটু ঘোরালে পুরো পাতা জোর করে পরের অংশে নিয়ে যাওয়ার নাম scroll hijacking, যা প্রায় সবাই অপছন্দ করেন। আর carousel-এর প্রথম স্লাইডের পরের জিনিসগুলো অনেকেই দেখেন না — গুরুত্বপূর্ণ তথ্য কখনো শুধু carousel-এর ভেতরে লুকাবেন না।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ফ্রন্ট-এন্ড ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন প্রায়ই আসে:
- Sticky header-এর নিচে
#idলিংকের শিরোনাম ঢাকা পড়ে — সমাধান? — scroll container-এ (সাধারণতhtml)scroll-padding-top, অথবা লক্ষ্যেscroll-margin-top. scroll-snap-type-এরmandatoryআরproximity-র পার্থক্য? — mandatory সবসময় কোনো খাঁজে থামায়; proximity শুধু খাঁজের কাছাকাছি থামলে টানে।- Scroll chaining কী, কীভাবে থামাবেন? — ভেতরের scroller সীমানায় পৌঁছালে স্ক্রল বাইরের scroller-এ চলে যাওয়া;
overscroll-behavior: contain. scroll()আরview()timeline-এর পার্থক্য? — scroll() একটি scroller কতটা স্ক্রল হয়েছে মাপে; view() একটি element জানালায় কতটা দেখা যাচ্ছে মাপে।- Scroll event-এর চেয়ে CSS scroll-driven animation কেন মসৃণ? — main thread-এর ওপর নির্ভর করে না; transform/opacity হলে compositor স্ক্রলের সাথেই মান বসায়।
৯ · AI-এর যুগে স্ক্রল ইফেক্ট
AI-কে “একটি সুন্দর স্ক্রল অ্যানিমেশন বানিয়ে দাও” বললে সাধারণত দুই রকম উত্তর আসে: হয় পুরোনো scroll event-ভিত্তিক JavaScript আর GSAP-এর মতো বড় লাইব্রেরি, নয়তো একেবারে নতুন CSS — কিন্তু কোনো @supports বা reduced motion ছাড়া। দুটিতেই আপনার যাচাই দরকার।
| AI-এর ভুল | ফল | ঠিক উপায় |
|---|---|---|
সাধারণ CSS-এ opacity: 0, animation দিয়ে দৃশ্যমান | পুরোনো ব্রাউজারে কনটেন্ট অদৃশ্য | লুকানো শুধু keyframe-এর from-এ, @supports-এর ভেতরে |
animation-timeline shorthand-এর আগে | স্ক্রল মানে না | shorthand-এর পরে লিখুন |
media query ছাড়া scroll-behavior: smooth আর parallax | reduced motion চাওয়া মানুষের অস্বস্তি | prefers-reduced-motion: no-preference-এর ভেতরে |
শুধু ::-webkit-scrollbar | Firefox-এ কিছুই বদলায় না | scrollbar-width / scrollbar-color |
scroll event-এ width বা top বদলানো | jank, এক ফ্রেম পিছিয়ে থাকা | CSS timeline বা IntersectionObserver; transform/opacity |
carousel-এ tabindex বা নাম নেই | কিবোর্ডে স্ক্রল অসম্ভব | tabindex="0", role="region", aria-label |
AI-কে প্রথম থেকেই শর্তগুলো বলে দিন — তাহলে উত্তর আধুনিক আর সবার জন্য ব্যবহারযোগ্য হবে:
AI-এর লেখা স্ক্রল-কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে শুধু কোড-লেখক নয়, ব্রাউজারের ভেতরটা বোঝা একজন ডেভেলপার বানাবে।
প্র ০১স্ক্রলের সাথে animation তো বহু বছর JavaScript দিয়েই হতো — তাহলে CSS-এ আলাদা একটি timeline আনার এত প্রয়োজন পড়ল কেন?
উত্তরটা লুকিয়ে আছে ব্রাউজারের ভেতরের “কর্মী-ভাগে”। প্রথম দিকের ব্রাউজারে সবকিছু — JavaScript চালানো, লেআউট, আঁকা, স্ক্রল — একটি thread-এ হতো। স্ক্রল করলে scroll event আসত, স্ক্রিপ্ট স্টাইল বদলাত, তারপর পাতা আঁকা হতো; সবকিছু একই লাইনে, তাই অন্তত মিলে থাকত।
স্মার্টফোন আসার পর সমস্যা স্পষ্ট হলো। আঙুলের সাথে পাতা না চললে ব্যবহারকারী সঙ্গে সঙ্গে টের পান। তাই ব্রাউজারগুলো ধীরে ধীরে স্ক্রলকে main thread থেকে আলাদা করে compositor-এর হাতে দিল: আগে আঁকা স্তরগুলো সরিয়ে দাও, JavaScript পরে খবর পাক। এতে স্ক্রল মসৃণ হলো, কিন্তু scroll event হয়ে গেল “পরে পাওয়া খবর”। ফলে JavaScript-এ বানানো parallax বা sticky ইফেক্ট এক-দুই ফ্রেম পিছিয়ে কাঁপতে শুরু করল। Firefox এমন পাতার জন্য console-এ “scroll-linked positioning effect” সতর্কবার্তা দেখাতে শুরু করে — অর্থাৎ ব্রাউজার নিজেই বলছিল, “এই কৌশল আমার নতুন নকশার সাথে মেলে না।”
মাঝপথের সমাধান ছিল position: sticky (একটি নির্দিষ্ট ইফেক্টকে CSS-এ আনা) আর IntersectionObserver (প্রতি পিক্সেলে নয়, শুধু সীমানায় খবর)। কিন্তু “স্ক্রলের প্রতিটি বিন্দুতে মসৃণ মান” — এর জন্য দরকার ছিল এমন একটি ব্যবস্থা, যেখানে ব্রাউজার আগে থেকেই জানে স্ক্রলের কোন অবস্থানে কোন মান বসবে। CSS-এর ঘোষণামূলক (declarative) ধরন ঠিক এটাই দেয়: আপনি নিয়ম বলেন, হিসাব করে ব্রাউজার — আর সেটি compositor-এর ভেতরেই করতে পারে। Houdini-র Animation Worklet-এর মতো আগের পরীক্ষা থেকে শিখে Scroll-driven Animations spec তৈরি হয়, আর ২০২৩ সালে Chromium প্রথম এটি সবার জন্য চালু করে।
মূল উপলব্ধি: ওয়েবে বহু নতুন CSS ফিচার আসলে “JavaScript-এ জনপ্রিয় হওয়া একটি কৌশলকে ব্রাউজারের ভেতরে নিয়ে আসা” — sticky, smooth scroll, snap, এখন timeline. যখন দেখবেন সবাই একই জোড়াতালি দিচ্ছে, বুঝবেন একটি নতুন মানদণ্ড আসছে।
প্র ০২Scroll snap-এর আজকের রূপ প্রথম চেষ্টাতেই আসেনি। পুরোনো নকশাটি কী ছিল, আর কেন বাতিল হলো?
প্রথম দিকের খসড়ায় snap-এর ধারণা ছিল নিয়মিত ব্যবধানের খাঁজ। ধারকে বলা হতো, “প্রতি 300px পরপর একটি খাঁজ” — scroll-snap-points-x: repeat(300px) ধরনের property দিয়ে, সাথে scroll-snap-destination আর scroll-snap-coordinate. Firefox আর তখনকার Internet Explorer/Edge এই পুরোনো রূপের বিভিন্ন অংশ চালু করেছিল, আর Safari -webkit- prefix দিয়ে।
সমস্যাটা বাস্তব জীবনে ধরা পড়ল। ওয়েবের কনটেন্ট সমান মাপের হয় না — একটি পণ্যের কার্ড 280px, পরেরটি লম্বা নামের কারণে 320px; পর্দা ঘোরালে বা ফন্ট বড় করলে সব মাপ বদলে যায়। “প্রতি 300px” তখন কার্ডের মাঝখানে খাঁজ কাটে। ডেভেলপারদের JavaScript দিয়ে মাপ হিসাব করে property আপডেট করতে হতো — যে ঝামেলা থেকে বাঁচতেই ফিচারটি আনা।
তাই CSS Working Group নকশাটি নতুন করে লেখে। নতুন চিন্তা: খাঁজ ঠিক করবে ভেতরের জিনিস নিজেই — scroll-snap-align দিয়ে, নিজের কিনারা বা মাঝখান ধরে। ধারক শুধু বলে অক্ষ আর কঠোরতা (scroll-snap-type). এতে কার্ডের মাপ যা-ই হোক, খাঁজ সবসময় কার্ডের সাথে থাকে; লেআউট বদলালে ব্রাউজার নিজেই হিসাব মেলায় — spec-এ এমনকি বলা আছে, লেআউট বদলের পর ব্রাউজার আগের snap-লক্ষ্যেই ফিরে যাবে (re-snapping). সাথে যোগ হয় snapport-এর জন্য scroll-padding, আর “বাক্সের চেয়ে বড় অংশে মুক্ত স্ক্রল” নিয়ম, যাতে লম্বা কনটেন্ট আটকে না যায়। পুরোনো property-গুলো ধীরে ধীরে ব্রাউজার থেকে সরানো হয়।
মূল উপলব্ধি: ভালো API নকশা তথ্যকে সেখানে রাখে, যেখানে সেটি স্বাভাবিকভাবে থাকে। খাঁজ কোথায়, সেটি কার্ড জানে, ধারক নয়। CSS-এর ইতিহাসে বহুবার দেখা গেছে, “আগে চালু করে পরে শেখা” — prefix আর পুরোনো খসড়া — দামি; সেজন্যই আজ ফিচার Baseline হওয়ার আগে Interop-এর মতো যৌথ পরীক্ষা হয়।
প্র ০৩স্ক্রলবার সাজানোর জন্য একটি সমৃদ্ধ, পরীক্ষিত পথ (::-webkit-scrollbar) থাকতেও মানদণ্ড এত “কম ক্ষমতার” দুটি property বেছে নিল কেন?
স্ক্রলবার সাজানোর ইচ্ছা নতুন নয়। ২০০০ সালের দিকে Internet Explorer 5.5 নিজস্ব scrollbar-face-color, scrollbar-arrow-color ধরনের property এনেছিল — সেই যুগের অনেক ব্যক্তিগত হোমপেজে রঙিন স্ক্রলবার ছিল ফ্যাশন। পরে WebKit আনে ::-webkit-scrollbar pseudo-element-গুচ্ছ, যা স্ক্রলবারের প্রতিটি অংশ — বোতাম, পথ, thumb, কোণ — আলাদা বাক্স হিসেবে সাজাতে দেয়। ক্ষমতা অনেক, তাই আজও বহু সাইটে এটি দেখবেন।
কিন্তু মানদণ্ড-নির্মাতারা এই পথ নেননি, কয়েকটি গভীর কারণে। (১) স্ক্রলবার প্ল্যাটফর্মের অংশ। Windows, macOS, Android, Linux — প্রতিটির স্ক্রলবার আলাদা: কোথাও স্থায়ী, কোথাও শুধু স্ক্রলের সময় ভাসমান (overlay). “স্ক্রলবারের বোতাম” বলে কিছু macOS-এ নেই-ই। প্রতিটি অংশের বাক্স-মডেল মানদণ্ডে বেঁধে দিলে সব প্ল্যাটফর্মকে একই কাঠামো মানতে হতো। (২) ব্যবহারযোগ্যতা। পূর্ণ ক্ষমতা মানে 2px চওড়া, পথের রঙে মিশে যাওয়া thumb বানানোর ক্ষমতাও — কম দৃষ্টিশক্তির বা কাঁপা হাতের মানুষের জন্য যা প্রায় অব্যবহারযোগ্য। (৩) ভবিষ্যৎ। ব্রাউজার যদি নিজের স্ক্রলবার উন্নত করতে চায়, অংশ-বাই-অংশ নিয়ন্ত্রণ সেটি আটকে দেয়।
তাই CSS Scrollbars spec শুধু দুটি “উদ্দেশ্য” জানাতে দেয় — রং আর মোটা/সরু/লুকানো — আর বাকিটা প্ল্যাটফর্মের হাতে রাখে। Firefox ২০১৮ সালের দিকে প্রথম এগুলো চালু করে; Chromium যোগ দেয় ২০২৪ সালে, আর সিদ্ধান্ত নেয় যে মানসম্মত property থাকলে -webkit- নিয়ম উপেক্ষা করবে — যাতে দুটি পথ মিশে বিশৃঙ্খলা না হয়।
মূল উপলব্ধি: মানদণ্ড প্রায়ই ইচ্ছাকৃতভাবে কম ক্ষমতা দেয়, যাতে ব্যবহারকারীর স্বার্থ আর প্ল্যাটফর্মের বৈচিত্র্য রক্ষা পায়। “আমি পুরো নিয়ন্ত্রণ চাই” — ডিজাইনারের এই চাওয়া আর “আমি স্ক্রলবারটা ধরতে পারছি না” — ব্যবহারকারীর এই অভিযোগের মাঝে ভারসাম্যই ভালো প্রকৌশল।
প্র ০৪Parallax আর স্ক্রল-অ্যানিমেশন কারও কারও জন্য শারীরিক অসুস্থতার কারণ হয় — এই উপলব্ধি ওয়েবে কীভাবে এলো, আর আজ ডেভেলপারের দায়িত্ব কী?
আমাদের ভারসাম্যের অনুভূতি আসে ভেতরের কানের vestibular ব্যবস্থা আর চোখের দেখা — এই দুই সংকেত মিলিয়ে। চোখ যদি বলে “চারপাশ সরছে” অথচ শরীর স্থির, তাহলে মস্তিষ্ক বিভ্রান্ত হয় — বাসে বসে বই পড়লে অনেকের যে বমিভাব হয়, সেটি এই অমিল থেকেই। vestibular disorder, মাইগ্রেন বা মস্তিষ্কে আঘাতের ইতিহাস আছে এমন মানুষের কাছে পর্দার বড় অংশ জুড়ে ভিন্ন গতির নড়াচড়া একই অমিল তৈরি করে — মাথা ঘোরা, বমিভাব, কখনো ঘণ্টার পর ঘণ্টা অসুস্থতা।
বড় পরিসরে এই আলোচনা জোরালো হয় ২০১৩ সালে, যখন Apple-এর iOS 7 ওয়ালপেপারে parallax আর অ্যাপ খোলায় বড় zoom animation আনে। অনেক ব্যবহারকারী অসুস্থ বোধ করার কথা জানান, আর Apple অ্যাক্সেসিবিলিটি সেটিংসে “Reduce Motion” বিকল্প যোগ করে। পরে অন্য অপারেটিং সিস্টেমেও একই ধরনের সেটিং আসে, আর ওয়েবে সেটি পৌঁছায় prefers-reduced-motion media feature হয়ে — ২০১৭ সালে Safari প্রথম এটি চালু করে, পরে বাকিরা। WCAG 2.1 যোগ করে 2.3.3 Animation from Interactions নির্দেশিকা।
লক্ষ করুন, “reduced motion” মানে “কোনো animation নয়” নয়। রং বদলানো, হালকা fade বা ছোট বোতামের প্রতিক্রিয়া সাধারণত সমস্যা করে না। সমস্যা করে বড় দূরত্বের নড়াচড়া, zoom, ঘোরা, parallax আর স্ক্রলের সাথে পর্দাজোড়া পরিবর্তন। তাই ভালো চর্চা হলো: reduced motion চাইলে নড়াচড়া (transform) সরিয়ে দিন, কিন্তু প্রয়োজনে হালকা opacity-বদল রাখুন — তথ্য যেন না হারায়।
আর ডিফল্ট কোনটি হবে? এই পর্বে আমরা “নড়াচড়া শুধু no-preference-এর ভেতরে” লিখেছি — অর্থাৎ নিরাপদ অবস্থাই ডিফল্ট, ইফেক্ট বাড়তি। এতে যে ব্রাউজার media feature-ই চেনে না, সেখানেও কিছু ভাঙে না।
মূল উপলব্ধি: অ্যাক্সেসিবিলিটি শুধু screen reader-এর বিষয় নয়, শরীরেরও। একটি “সুন্দর” ইফেক্ট কারও জন্য বিকেলটা নষ্ট করে দিতে পারে — নড়াচড়াকে সবসময় ঐচ্ছিক, ব্যবহারকারীর নিয়ন্ত্রণে থাকা একটি সুবিধা হিসেবে ভাবুন।
প্র ০৫“টেনে রিফ্রেশ” আর স্ক্রল-শিকল (scroll chaining) — এমন ছোট আচরণ নিয়ে একটি আলাদা CSS property কেন দরকার হলো?
Pull-to-refresh আজ এত স্বাভাবিক যে মনে হয় চিরকাল ছিল। আসলে এটি জনপ্রিয় করেন Loren Brichter, ২০০৮–০৯ সালের দিকে তাঁর iPhone-এর Twitter অ্যাপ “Tweetie”-তে। পরে Twitter অ্যাপটি কিনে নেয়, আর ইশারাটি প্রায় সব মোবাইল অ্যাপে ছড়িয়ে পড়ে। মোবাইল ব্রাউজারগুলোও পাতার জন্য এটি গ্রহণ করে — Android-এর Chrome-এ পাতার ওপরে টানলে রিলোড হয়।
সমস্যা দেখা দিল যখন ওয়েব শুধু “পাতা” না থেকে “অ্যাপ” হয়ে উঠল। একটি চ্যাট অ্যাপে মেসেজ তালিকা নিজেই একটি scroller; ওপরে টেনে পুরোনো মেসেজ আনতে গেলে ব্রাউজার ভাবত পাতা রিলোড করতে বলছেন। আঁকার অ্যাপে নিচে টানলে আঁকা নষ্ট। ম্যাপে আঙুল টানলে পাতাই সরে যেত। আর ছোট modal-এর শেষে পৌঁছে পেছনের পাতা স্ক্রল হয়ে যাওয়া — scroll chaining — ব্যবহারকারীকে বিভ্রান্ত করত।
আগের সমাধান ছিল JavaScript-এ touchmove বা wheel event ধরে preventDefault() — সীমানায় পৌঁছালে স্ক্রল আটকানো। কিন্তু এতে একটি বড় দাম ছিল: ব্রাউজারকে প্রতিটি স্পর্শে অপেক্ষা করতে হতো, “স্ক্রিপ্ট কি স্ক্রল আটকাবে?” — ফলে compositor-এর মসৃণ স্ক্রল ভেস্তে যেত। (এ কারণেই পরে passive event listener-এর ধারণা আসে, যেখানে আপনি আগেই কথা দেন যে আটকাবেন না।) overscroll-behavior সেই সিদ্ধান্তটা আগেই CSS-এ ঘোষণা করার সুযোগ দেয়: ব্রাউজার আগে থেকেই জানে এই বাক্সের সীমানায় কী করতে হবে, কোনো স্ক্রিপ্টের অপেক্ষা ছাড়াই।
মূল উপলব্ধি: ব্রাউজারকে “আগে জানিয়ে রাখা” (declarative) প্রায়ই “ঘটনার সময় থামিয়ে দেওয়ার” (imperative) চেয়ে দ্রুত ও নিরাপদ। loading="lazy", passive listener, overscroll-behavior, scroll-driven animation — সবগুলোর পেছনে একই দর্শন: উদ্দেশ্য বলুন, বাস্তবায়ন ব্রাউজারকে করতে দিন।
অনুশীলন
বেশিরভাগ কাজ উপরের লাইভ এডিটরে বা নিজের ব্রাউজারে করে দেখা যায়।
-
মাঝখানে বসা carousel: পণ্যের carousel-এ এমন পরিবর্তন করুন, যাতে প্রতিটি কার্ড বাক্সের ঠিক মাঝখানে থামে, আর জোরে সোয়াইপেও একবারে একটির বেশি কার্ড না পেরোয়। কোন দুটি লাইন বদলাবেন বা যোগ করবেন?
.item-এscroll-snap-align: center;আরscroll-snap-stop: always;. প্রথমটি কার্ডের মাঝখানকে snapport-এর মাঝখানে মেলায়, দ্বিতীয়টি কোনো খাঁজ পেরোনো নিষেধ করে। চাইলেflex-basis70% থেকে 60% করুন — দুই পাশে প্রতিবেশী কার্ডের টুকরো আরও স্পষ্ট দেখা যাবে, যা “আরও আছে” বোঝায়। মনে রাখুন,scroll-snap-stop: alwaysব্যবহারকারীকে ধীর করে; ২০টি পণ্যের সারিতে এটি বিরক্তিকর হতে পারে। -
ভুল খুঁজুন: একজন লিখেছেন
.bar { animation-timeline: scroll(); animation: grow linear; }— দাগ স্ক্রল মানছে না। দুটি ভুল কী? (ইঙ্গিত: একটি ক্রমের, একটি সাপোর্টের)(১)
animation-timelineলেখা হয়েছে shorthand-এর আগে —animationshorthand তাকে আবারauto(সময়ের ঘড়ি) করে দেয়, আর সময়কালautoহওয়ায় animation চোখের পলকে শেষ হয়ে যায়। ক্রম উল্টে দিন। (২)@supportsনেই — যে ব্রাউজারanimation-timelineচেনে না, সেখানেও একই ঘটনা ঘটবে: দাগ পুরো চওড়া হয়ে বসে থাকবে, যা পাঠককে ভুলভাবে বলবে “পড়া শেষ”। ঠিক রূপ:animation: grow linear both; animation-timeline: scroll();, আর পুরোটা@supports (animation-timeline: scroll())-এর ভেতরে, দাগ ডিফল্টে লুকানো।bothfill রাখাও ভালো অভ্যাস — বিশেষ করেview()-এর range-এর বাইরে প্রথম ও শেষ অবস্থা ধরে রাখতে। -
Scroll chaining খুঁজে দেখুন: আপনার প্রিয় কয়েকটি বাংলাদেশি ই-কমার্স বা সংবাদ সাইটে চ্যাট বক্স, ফিল্টার মেনু বা ছোট স্ক্রল-বাক্স খুলুন। শেষ পর্যন্ত স্ক্রল করে আরও ঘোরান। পেছনের পাতা নড়ে কি? DevTools দিয়ে বাক্সটির
overscroll-behaviorদেখুন।অনেক সাইটে দেখবেন পেছনের পাতা নড়ে — অর্থাৎ
overscroll-behaviorডিফল্টauto. DevTools-এ Elements প্যানেলে বাক্সটি বেছে Computed ট্যাবেoverscroll-behaviorখুঁজুন। Styles প্যানেলে নিজেoverscroll-behavior: containযোগ করে আবার চেষ্টা করুন — পার্থক্যটা সঙ্গে সঙ্গে টের পাবেন। কিছু সাইট modal খুললে পুরোbody-তেoverflow: hiddenদেয় — এটিও একটি পুরোনো সমাধান, তবে এতে স্ক্রলবার সরে গিয়ে লেআউট লাফ দিতে পারে, যেটিscrollbar-gutter: stableঠেকায়। -
বেরোনোর সময় ঝাপসা: খবরের কার্ডের উদাহরণে এমন পরিবর্তন করুন, যাতে কার্ড শুধু ঢোকার সময় নয়, ওপর দিয়ে বেরোনোর সময়ও ঝাপসা হয়ে যায়। (ইঙ্গিত: একটি keyframe-এ একাধিক শতাংশ, আর
animation-range-এentryথেকেexitপর্যন্ত।)keyframe-এ তিনটি অবস্থা দিন, আর range পুরো যাত্রা (
cover) জুড়ে রাখুন:@keyframes inout { 0% { opacity: 0; transform: translateY(40px); } 25%, 75% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateY(-40px); } } .card { animation: inout linear both; animation-timeline: view(); animation-range: cover 0% cover 100%; }এখানে 25% আর 75%-এর মাঝে কার্ড পুরো স্পষ্ট। আরও নিখুঁত চাইলে দুটি আলাদা animation লিখুন — একটি
entry, একটিexitrange-এ — কমা দিয়ে:animation-range: entry, exit;. পুরো অংশটি আগের মতোই@supportsআরno-preference-এর ভেতরে রাখতে ভুলবেন না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমনিচে স্ক্রল করার সময় একটি ছোট কার্ডের view() timeline-এর মুহূর্তগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ⬆️ কার্ডের ওপরের কিনারা জানালার নিচের কিনারা ছুঁলো (
entry 0%) - 🖼️ পুরো কার্ড জানালার ভেতরে এলো (
entry 100%) - ⤴️ কার্ডের ওপরের কিনারা জানালার ওপর দিয়ে বেরোতে শুরু করল (
exit 0%) - 🚪 পুরো কার্ড জানালার ওপর দিয়ে বেরিয়ে গেল (
exit 100%)
পাতায় একটি sticky header আছে। মেনুর #price লিংকে চাপলে “দাম” শিরোনামটি header-এর নিচে ঢাকা পড়ে। সবচেয়ে সহজ CSS সমাধান কোনটি?
- শিরোনামে
z-index: 10 html { scroll-padding-top: 4rem; }html { scroll-behavior: smooth; }html { overscroll-behavior: contain; }
একটি আড়াআড়ি carousel বানাতে scroll-snap-type: x mandatory কোন element-এ লিখতে হয়?
- প্রতিটি কার্ডে
<body>-তে- যে বাক্সে
overflow-x: autoআছে, সেই ধারকে - শুধু প্রথম কার্ডে
একটি চ্যাট বক্সের শেষে পৌঁছে আরও স্ক্রল করলে পেছনের পাতা স্ক্রল হয়ে যায়। চ্যাট বক্সে কোনটি লিখলে এটি থামবে, কিন্তু চ্যাটের নিজের স্বাভাবিক সীমানা-ইফেক্ট থাকবে?
overscroll-behavior: containoverscroll-behavior: noneoverflow: hiddenscroll-snap-stop: always
.bar { animation-timeline: scroll(); animation: grow linear both; } — দাগটি স্ক্রলের সাথে বাড়ে না। কেন?
scroll()-এর ভেতরে কিছু লেখা নেইlinearব্যবহার করা যায় না- সময়কাল (যেমন
2s) দেওয়া হয়নি animationshorthand পরে আসায়animation-timelineমুছে গেছে
২০২৫–২৬ সালে CSS scroll-driven animation (animation-timeline: view())-এর সাপোর্ট নিয়ে কোন কথাটি সঠিক?
- সব প্রধান ব্রাউজারে বহু বছর ধরে আছে — Baseline Widely available
- এটি not yet Baseline;
@supportsদিয়ে progressive enhancement হিসেবে ব্যবহার করা উচিত - শুধু
-webkit-animation-timelineলিখলে সব ব্রাউজারে চলে - ফিচারটি বাতিল হয়ে গেছে, শুধু JavaScript-ই উপায়
একটি স্ক্রল-বাক্সের ভেতরে শুধু ছবি আর লেখা আছে (কোনো লিংক বা বোতাম নেই)। কিবোর্ড ব্যবহারকারী যেন এটি স্ক্রল করতে পারেন, সেজন্য কোনটি সবচেয়ে উপযুক্ত?
scrollbar-width: nonetabindex="-1"tabindex="0"সাথেrole="region"আর একটিaria-labelaria-hidden="true"
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- প্রতিটি স্ক্রল ইফেক্ট একটি scroll container-এর; property-টি ধারকের না ভেতরের জিনিসের, আগে সেটি ঠিক করুন।
scroll-behavior: smoothশুধু reduced motion না চাইলে; sticky header-এর জন্যscroll-padding-topবাscroll-margin-top.- Scroll snap: ধারকে
scroll-snap-type(অক্ষ + mandatory/proximity), ভেতরেscroll-snap-align, দরকারেscroll-snap-stop: always. overscroll-behavior: containscroll chaining থামায়; স্ক্রলবার সাজাতেscrollbar-width/scrollbar-color,::-webkit-scrollbarশুধু পুরোনো বিকল্প।animation-timeline: scroll()/view()+animation-range— shorthand-এর পরে লিখুন,@supportsআর reduced motion-এর ভেতরে রাখুন, কনটেন্ট কখনো ডিফল্টে লুকাবেন না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৩ · ফর্ম স্টাইলিং আর UX — সুন্দর, পরিষ্কার, ব্যবহারযোগ্য ফর্মপরবর্তী পর্বফর্মের নিয়ন্ত্রণ সাজানো আর ভুলের বার্তার নকশা।
- HTML ও CSS · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ২৩ · Normal flow, display আর overflowফিরে দেখুনscroll container কীভাবে তৈরি হয়,
scrollbar-gutter. - পর্ব ২৪ · position আর z-indexফিরে দেখুনsticky header আর
scroll-padding-top-এর প্রথম পরিচয়। - JavaScript · Eventপাশের ট্র্যাকscroll event, listener আর কেন ভারী কাজ main thread আটকে দেয়।